Spread operator type checking is too general for generics.

huangapple go评论61阅读模式
英文:

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);

Playground

有没有更好的方法来强类型化这种情况?或者这是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 = &lt;T extends MaybeHasId&gt;(item: T, newId?: string): T =&gt; {
  return {...item, id: newId}
}

const moreSpecificObject: HasId = replaceId({id: &quot;specific id&quot;}, undefined);
console.log(moreSpecificObject.id.length);

Playground

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: &quot;two&quot; }, 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&lt;T extends { a: number, b: string }&gt;(t: T) {
  return { ...t, b: 2 };
}
/* function generic&lt;T extends { a: number; b: string;}&gt;(
      t: T
   ): T &amp; { 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 &amp; {id: string | undefined}, and is therefore assignable to T:

const replaceId = &lt;T extends MaybeHasId&gt;(item: T, newId?: string): T =&gt; {
  const ret = { ...item, id: newId };
  // const ret: T &amp; { 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 = &lt;T extends MaybeHasId&gt;(item: T, newId?: string) =&gt; {
  const ret = { ...item, id: newId };
  // const ret: T &amp; { id: string | undefined; }
  return ret as Omit&lt;T, &quot;id&quot;&gt; &amp; { id: string | undefined }
}

Now you'll get the error you were expecting:

const moreSpecificObject: HasId = replaceId({ id: &quot;specific id&quot; }, undefined);
// -&gt; ~~~~~~~~~~~~~~~~~~
// Type &#39;undefined&#39; is not assignable to type &#39;string&#39;.

Playground link to code

huangapple
  • 本文由 发表于 2023年4月7日 02:36:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/75952723.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定