从类型的默认参数推断 TypeScript 类型

TypeScript type inference from default parameter of type

给定以下示例:

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

const foo: Foo = { name: "foo" };
const bar: Bar = { name: "bar", displayName: "dn" };

const getName = <T extends Foo>(obj: T = foo): string => {
  return obj.name;
};

getName(foo); // Ok
getName(bar); // Ok

obj: T = foo 导致错误。即使 T extends Foo,这也不被接受。为什么会这样?有什么解决方法?

Example.

您的 getName 函数不需要 Generic,并且可以对输入参数 obj 使用类型注释 Foo,即:

const getName = (obj: Foo = foo): string => {
  return obj.name;
};

这是因为在 Typescript 中,每个复杂类型(例如对象和数组)在其成员中都是 协变

换句话说:由于 TypeScript 知道 Foo 这里的必填字段较少,因此 BarFoo 的子类型,任何 Bar 类型的元素都将是被 getName 函数接受。也就是说,您可以在任何需要 Foo 的地方安全地使用 Bar。 (正如@DDomen 在评论中指出的那样,函数参数类型的行为不同)

让我解释一下协方差:

  • 虽然您将类型 Foo 分配给 obj,但它也适用于 getName(bar);,因为 Bar extends Foo(因此 BarFoo).

    的子类型
  • 由于 Typescript 工程师设置类型系统的方式,这是允许的。他们决定应该允许对象的成员(即形状和数组等复杂类型)接收其定义的类型以及任何属于该类型的子类型。

  • 在类型论中,这种行为可以描述为形状在其成员中协变

  • 其他类型系统不允许这种灵活性,即在这种情况下使用子类型。这样的类型系统将在形状的成员上调用 invariant,因为它们需要完全相同的类型 Foo,并且不允许 TypeScript 允许的这种灵活性。

  • 另请注意,Bar 不必显式扩展 Foo,因为 TypeScript 是结构类型的。 TypeScript 比较两个对象的结构,看一个是另一个的子类型还是超类型。因此,除了 interface Bar extends Foo 之外,您还可以将 Bar 定义为:

    interface Bar {
      name: string;
      displayName: string;
    }
    

    getName 的参数 obj 仍将正确键入为 Foo.

何时使用泛型?

您不需要泛型,因为您不想将函数的不同部分相互关联。如果您想将输入参数与输出参数相关联,则可以有效使用泛型。

查看您的代码 TS Playground

另请参阅 this related issue 关于您遇到的错误,即

Type 'Foo' is not assignable to type 'T'.
  'Foo' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Foo'.(2322)

问题是 foo 不是 T 类型,假设你想用接口 Bar:

实例化泛型
const getName = (obj: Bar = foo): string => {
  return obj.name;
};

您可以看到 foo 不是 Bar(缺少 displayName 属性)并且您不能将其分配给类型为 Bar.

您现在有三个选择:

  1. 强制 foo 用作通用 T<T extends Foo>(obj: T = foo as T): string => ...。但这是你能做的最糟糕的选择(可能导致意想不到的 results/errors)。

  2. 重载您的函数以匹配您的通用规范。如果您需要 return(并实际使用)您的通用实例,这将很有用:

const getName: {
  // when you specify a generic on the function
  // you will use this overload
  <T extends Foo>(obj: T): T;

  // when you omit the generic this will be the overload
  // chosen by the compiler (with an optional parameter)
  (obj?: Foo): Foo;
}
= (obj: Foo = foo): string => ...;
  1. 请注意,您的函数根本不使用泛型并将参数作为简单的 Foo: (obj: Foo = foo): string => ... 传递。这应该是您情况中最好的一个