英文:
Spread operator type checking is too general for generics
问题
在处理接口继承和泛型时,我遇到了一些奇怪的行为,可能会导致运行时问题。这是在最新版本的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的一个潜在问题?我希望要么在这段代码中有一个编译时错误,要么在编写基于继承的类型时“扩展”更加严格。
英文:
While working with interface inheritance and generics, I ran across some odd behavior that can result in runtime issues. This is in the most recent version of TypeScript, 5.0.3. In short, a function accepting a generic that extends an underlying interface appears to allow improperly typed values to be returned.
Here's a reproduction of the issue:
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);
Is there a better way to strongly type situations like this? Or is it an underlying issue with TypeScript? I'd either expect there to be a compile-time error with this code, or for "extending" to be more strict while writing inheritance-based types.
答案1
得分: 1
很抱歉,我无法理解你的请求。请提供更多信息或明确你需要的帮助。
英文:
Unfortunately TypeScript doesn't have a type operator {...T, ...U}
that truly corresponds with the results of object spread, especially those involving overwritten properties. See the relevant feature request at microsoft/TypeScript#10727 as well as issues linked to it like microsoft/TypeScript#50185 and microsoft/TypeScript#50559.
When spreading objects of specific known types, the compiler suppresses overwritten properties from the result:
const specific = { ...{ a: 1, b: "two" }, b: 2 };
/* const specific: {
b: number;
a: number;
} */
But when spreading values of generic types, the compiler approximates the result as an intersection type, as implemented in 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; }
*/
As mentioned in microsoft/TypeScript#28234,
> An alternative to using intersections would be to introduce a higher-order type operator { ...T, ...U }
along the lines of what #10727 explores. While this appears attractive at first, it would take a substantial amount of work to implement this new type constructor and endow it with all the capabilities we already implement for intersection types, and it would produce little or no gain in precision for most scenarios. In particular, the differences really only matter when spread expressions involve objects with overlapping property names that have different types. Furthermore, for an unconstrained type parameter T
, the type { ...T, ...T }
wouldn't actually be assignable to T
, which, while technically correct, would be pedantically annoying.
>
>Having explored both options, and given that intersection types are already used for Object.assign and JSX literals, we feel that intersection types strike the best balance between accuracy and complexity for this feature.
So that's what's going on with your code. The value { ...item, id: newId }
is seen as having the intersection type T & {id: string | undefined}
, and is therefore assignable to T
:
const replaceId = <T extends MaybeHasId>(item: T, newId?: string): T => {
const ret = { ...item, id: newId };
// const ret: T & { id: string | undefined; }
return ret;
}
While you can't fix this in general, you could address it in your example code by using a type assertion to synthesize a more accurate type via the Omit
utility type:
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 }
}
Now you'll get the error you were expecting:
const moreSpecificObject: HasId = replaceId({ id: "specific id" }, undefined);
// -> ~~~~~~~~~~~~~~~~~~
// Type 'undefined' is not assignable to type 'string'.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论