typescript - What is the idiomatic way in TypeScript to extend an object (already implementing an interface) to a wider interface?
问题描述
I have these DTO interfaces in TypeScript:
interface ProductDto {
readonly productId: number;
readonly name : string;
}
interface FirstPartyProductDto extends ProductDto {
readonly foo: string;
readonly bar: number;
}
My application primarily uses server-side rendering but behaves like an SPA (this project is not using any SPA frameworks like Angular, Vue, etc). To aid the rehydration-process when the browser loads the page additional data is rendered to data-
attributes.
So if a page contains a list-of-products, it would be rendered like so:
<ul class="productsList">
<li
data-product-id="1"
data-product-name="Exploding toilet seat"
>
</li>
<li
data-product-id="2"
data-product-name="Brussels sprout dispenser"
>
</li>
<li
data-product-id="3"
data-product-name="Battery-tester tester"
>
</li>
</ul>
And the TypeScript to rehydrate ProductDto
is straightforward:
static loadProductFromHtmlElement( e: HTMLElement ): ProductDto {
return loadProductFromDataSet( e.dataset );
}
static loadProductFromDataSet( d: DOMStringMap ): ProductDto {
return {
productId: parseInt( d['productId']!, 10 ),
name : d['productName']!
};
}
Now, supposing I want to rehydrate FirstPartyProductDto
instances, then my code currently looks like this:
static loadFirstPartyProductFromDataSet( d: DOMStringMap ): FirstPartyProductDto {
const productDto = loadProductFromDataSet( d );
return {
// ProductDto members:
productId: productDto.productId,
name : productDto.name,
// FirstPartyProductDto members:
foo : d['foo']!,
bar : parseInt( d['bar']!, 10 )
};
}
I don't like how my code manually repeats the members of ProductDto
as it populates the new returned object.
If this were untyped JavaScript, I could simply extend the existing productDto
object instead:
static loadFirstPartyProductFromDataSet( d: DOMStringMap ): FirstPartyProductDto {
const productDto = loadProductFromDataSet( d );
productDto.foo = d['foo']!;
productDto.bar = parseInt( d['bar']!, 10 );
return productDto;
}
But the above code won't work because productDto
is typed as ProductDto
and so doesn't have the foo
and bar
properties, and even if I cast productDto
as FirstPartyProductDto
TypeScript won't let me assign those properties because they're readonly
.
The only alternative I can immediately think of is to just cast productDto
to any
, but that means losing type-safety altogether.
There is also both Object.assign
and the object spread operator ...
which TypeScript supports, which certainly improves loadFirstPartyProductFromDataSet
by avoiding the need to type-out all of the inherited properties...
function loadFirstPartyProductFromDataSet( d: DOMStringMap ): FirstPartyProductDto {
const productDto = loadProductFromDataSet( d );
return {
...productDto,
foo: d['foo']!,
bar: parseInt( d['bar']!, 10 )
};
}
...but it's still copying properties and values to a new object rather than setting properties on the existing object.
解决方案
唯一readonly
阻止属性的事情是显式设置属性值。它们不影响可分配性;您可以将类型值分配给类型{a: string}
变量,{readonly a: string}
反之亦然(有关更多信息,请参阅microsoft/TypeScript#13447)。这意味着我们可以使用类型函数,例如
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
它剥离readonly
了属性,并编写了您尝试使用类型断言的代码版本(这就是您所说的“强制转换”):
static loadFirstPartyProductFromDataSetAssert(d: DOMStringMap): FirstPartyProductDto {
const productDto = Blah.loadProductFromDataSet(d) as Mutable<FirstPartyProductDto>;
productDto.foo = d.foo!;
productDto.bar = parseInt(d.bar!, 10);
return productDto;
}
这可能是最简单的方法。但是,它不是完全类型安全的,您必须注意实际设置断言的额外属性:
static loadFirstPartyProductFromDataSetAssertBad(d: DOMStringMap): FirstPartyProductDto {
const productDto = Blah.loadProductFromDataSet(d) as Mutable<FirstPartyProductDto>;
productDto.foo = d.foo!;
// oops, forgot bar
return productDto; // no error here
}
当您添加属性时,您可以使用用户定义的断言函数来表示对象类型的逐渐缩小,从而获得更多的安全性,称为set(obj, key, val)
. 它将像这样使用:
static loadFirstPartyProductFromDataSet(d: DOMStringMap): FirstPartyProductDto {
const productDto = Blah.loadProductFromDataSet(d);
// const productDto: ProductDto
set(productDto, "foo", d['foo']!);
// const productDto: {
// readonly productId: number;
// readonly name: string;
// foo: string;
// }
set(productDto, "bar", parseInt(d['bar']!, 10));
// const productDto: {
// readonly productId: number;
// readonly name: string;
// foo: string;
// bar: number;
// }
return productDto; // okay
}
并且您可以验证如果您遗漏了它会给您一个错误"bar"
。我使用的具体set()
定义为:
function set<T extends { [k: string]: any } & { [P in K]?: never }, K extends PropertyKey, V>(
obj: T, key: K, val: V
): asserts obj is Extract<(T & Record<K, V> extends infer O ? { [P in keyof O]: O[P] } : never), T> {
(obj as any).key = val;
}
这对于您的目的来说可能太复杂了,但我只是想证明可以编写代码,将属性添加到现有变量并让类型系统了解您在做什么。
推荐阅读
- c# - 如何本地化派生属性的显示名称?
- java - 包含 RestTemplate 交换调用的 void 方法的 JUnit 测试
- linux - Linux GPU服务器上的无效设备序号错误?
- perl - 如何在 return 语句之后调用另一个子程序
- java - 了解跨集群环境的静态变量
- python - 如何获得查询集的不同值?
- reactjs - React reload after peak so photo that peak to OCR Process in无法进动
- django - Django错误:[
] - javascript - 如果我想进行新的计算,为什么在安装重置按钮后显示“NaN”?
- excel - 组合框列表填充范围来自 2 个不同的工作表