在处理接口继承和泛型时,我遇到了一些可能导致运行时问题的奇怪行为。这是TypeScript的最新版本,5.0.3。简而言之,接受扩展底层接口的泛型的函数似乎允许返回类型不正确的值。
以下是该问题的复制:
interface MaybeHasId {
id?: string,
}
interface HasId extends MaybeHasId {
id: string,
}
const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
return {...item, id: newId}
}
const moreSpecificObject: HasId = replaceId({id: "specific id"}, undefined);
console.log(moreSpecificObject.id.length);
游乐场
有更好的方法来强类型这样的情况吗?或者这是TypeScript的一个潜在问题?我希望这段代码要么存在编译时错误,要么存在"扩展"错误。在编写基于继承的类型时更严格。
不幸的是,TypeScript没有真正对应于对象扩展结果的类型操作符{...T, ...U}
,特别是那些涉及覆盖属性的操作符。请参阅相关的特性请求:microsoft/TypeScript#10727,以及与之相关的问题,如microsoft/TypeScript#50185和microsoft/TypeScript#50559。
当扩展特定已知类型的对象时,编译器会从结果中抑制被覆盖的属性:
const specific = { ...{ a: 1, b: "two" }, b: 2 };
/* const specific: {
b: number;
a: number;
} */
但是当扩展泛型类型的值时,编译器会将结果近似为交叉类型,如microsoft/TypeScript#28234:
所实现的那样:function generic<T extends { a: number, b: string }>(t: T) {
return { ...t, b: 2 };
}
/* function generic<T extends { a: number; b: string;}>(
t: T
): T & { b: number; }
*/
正如在microsoft/TypeScript#28234中提到的,
使用交集的另一种选择是引入一个高阶类型运算符
{ ...T, ...U }
,类似于#10727所探讨的内容。虽然这看起来很吸引人,但要实现这个新的类型构造函数并赋予它我们已经为交叉类型实现的所有功能,需要做大量的工作,而且在大多数情况下,它在精度上几乎没有提高。特别是,只有当扩展表达式涉及具有重叠属性名称的对象时,这些差异才真正重要,这些对象具有不同的类型。此外,对于无约束类型参数T
,类型{ ...T, ...T }
实际上不能赋值给T
,这虽然在技术上是正确的,但在学院派上是令人讨厌的。已经探索了这两个选项,并且考虑到交叉类型已经用于Object。指定和JSX文字,我们认为交叉类型在准确性和复杂性之间达到了最好的平衡。
这就是你的代码。值{ ...item, id: newId }
被视为与T & {id: string | undefined}
有交集,因此可以赋值给T
:
const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
const ret = { ...item, id: newId };
// const ret: T & { id: string | undefined; }
return ret;
}
虽然一般不能解决这个问题,但可以在示例代码中通过使用类型断言来通过Omit
实用程序类型合成更准确的类型来解决这个问题:
const replaceId = <T extends MaybeHasId>(item: T, newId?: string) => {
const ret = { ...item, id: newId };
// const ret: T & { id: string | undefined; }
return ret as Omit<T, "id"> & { id: string | undefined }
}
现在你将得到你所期望的错误:
const moreSpecificObject: HasId = replaceId({ id: "specific id" }, undefined);
// -> ~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'.
Playground链接到代码