如何解决TS2322: "could be instantiated with a different subtype of constraint"

7,667 阅读4分钟

遇到问题

最近使用 ts 写个工具类函数时, 遇到了 ts 报错:

function renameKeys<T extends { [key: string]: unknown }>(
  keysMap: { [key: string]: string },
  obj: T
): T {
  return Object.keys(obj).reduce(
    (acc, key) => ({
      ...acc,
      ...{ [keysMap[key] || key]: obj[key] }
    }),
    {}
  )
}

CleanShot 2021-09-26 at 16.08.22@2x.png

问题出在函数返回的泛型 T.

随后我将泛型 T 改为 { [key: string]: unknown } , 报错消失了.

function renameKeys<T extends { [key: string]: unknown }>(
  keysMap: { [key: string]: string },
  obj: T
): { [key: string]: unknown } {
  return Object.keys(obj).reduce(
    (acc, key) => ({
      ...acc,
      ...{ [keysMap[key] || key]: obj[key] }
    }),
    {}
  )
}

这就很奇怪, { [key: string]: unknown } 本身就为 T 的约束, 用 { [key: string]: unknown } 和用 T 有什么区别吗?

在我一顿 Google 之后, 在一篇文章中明白了其中道理.

理解TS报错信息

下面我将分解错误消息的每句话:

Type '{}' is not assignable to type 'T'.
  '{}' is assignable to the constraint of type 'T', but 'T' could be instantiated with  a different subtype of constraint '{ [key: string]: unknown; }'

Type '{}' 什么意思?

这个类型可以分配任何值,除了 nullundefined。例如:

type A = {}
const a0: A = undefined // error 
const a1: A = null // error 
const a2: A = 2 // ok 
const a3: A = 'hello world' //ok 
const a4: A = { foo: 'bar' } //ok 
// and so on...

is not assignable 什么意思?

分配是实例与类型相匹配。如果你的实例不匹配类型,你会得到一个错误。例如:

// type string is not assignable to type number 
const a: number = 'hello world' //error 

// type number is assinable to type number 
const b: number = 2 // ok

a different subtype 什么意思?

  • A 是 S 的子类型: 类型 A 在类型 S 的基础上增加了额外属性.
  • A 和 B 是 S 的不同子类型: 类型 A 与类型 B 分别在类型 S 的基础上增加了不同的额外属性.

例如: 下面代码的情况是

  1. A 和 D 是相同的类型
  2. B 是 A 的子类型
  3. E 不是 A 的子类型
  4. B 和 C 是 A 的不同子类型
type A = { readonly 0: '0'} 
type B = { readonly 0: '0', readonly foo: 'foo'} 
type C = { readonly 0: '0', readonly bar: 'bar'} 
type D = { readonly 0: '0'} 
type E = { readonly 1: '1', readonly bar: 'bar'}
type A = number 
type B = 2 
type C = 7 
type D = number 
type E = `hello world`
type A = boolean 
type B = true 
type C = false 
type D = boolean 
type E = number

当你在 ts 中使用 type 关键字时, 例如: type A = { foo: 'Bar' }, 那么 A 指向的是该值的结构.

constraint of type 'T' 什么意思?

类型约束仅仅是你放在 extends 关键字右侧的内容。在下面的例子中,类型约束是'B'。

const func = <A extends B>(a: A) => `hello!`

所以, Type 'B' is the constraint of type 'A'.

类型约束 extends

为了说明这一点,我将展示三种情况。在每种情况下唯一会变化的是类型约束,其他什么都不会改变。

我想让你注意的是,类型约束不会限制其子类型。看以下示例:

Given:

type Foo = { readonly 0: '0'} 
type SubType = { readonly 0: '0', readonly a: 'a'} 
type DiffSubType = { readonly 0: '0', readonly b: 'b'} 
const foo: Foo = { 0: '0'}
const foo_SubType: SubType = { 0: '0', a: 'a' } 
const foo_DiffSubType: DiffSubType = { 0: '0', b: 'b' }

CASE 1: 无类型约束

const func = <A>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // ok
const c1 = func(null) // ok
const c2 = func(() => undefined) // ok
const c3 = func(10) // ok
const c4 = func(`hi`) // ok
const c5 = func({}) //ok
const c6 = func(foo) // ok
const c7 = func(foo_SubType) //ok
const c8 = func(foo_DiffSubType) //ok

CASE 2: 一般的类型约束

在 Typescript 中,类型约束不会限制其子类型.

const func = <A extends Foo>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // error
const c1 = func(null) // error
const c2 = func(() => undefined) // error
const c3 = func(10) // error
const c4 = func(`hi`) // error
const c5 = func({}) // error
const c6 = func(foo) // ok
const c7 = func(foo_SubType) // ok  <-- Allowed
const c8 = func(foo_DiffSubType) // ok <-- Allowed

CASE 3: 更具体的约束

const func = <A extends SubType>(a: A) => `hello!`

// call examples
const c0 = func(undefined) // error
const c1 = func(null) // error
const c2 = func(() => undefined) // error
const c3 = func(10) // error
const c4 = func(`hi`) // error
const c5 = func({}) // error
const c6 = func(foo) // error <-- Restricted now
const c7 = func(foo_SubType) // ok  <-- Still allowed
const c8 = func(foo_DiffSubType) // error <-- NO MORE ALLOWED !

总结示例

以下函数:

const func = <A extends Foo>(a: A = foo_SubType) => `hello!` //error!

产生如下错误信息:

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

因为 Typescript 是从函数调用中推断出 A,并且在语言中并没有限制你用不同的 'Foo' 子类型来调用函数。例如,下面的所有函数调用都被认为是有效的:

const c0 = func(foo)  // ok! type 'Foo' will be infered and assigned to 'A'
const c1 = func(foo_SubType) // ok! type 'SubType' will be infered
const c2 = func(foo_DiffSubType) // ok! type 'DiffSubType' will be infered

因此,将具体类型赋值给泛型类型形参是不正确的,因为在 TS 中,类型形参总是可以实例化为任意不同的子类型。

结论: 永远不要将具体类型赋给泛型类型参数,将其视为只读类型!

相反, 这样做:

const func = <A extends Foo>(a: A) => `hello!` //ok!

结论

  1. 泛型是函数运行时推断出的类型;
  2. 不要给泛型类型的形参设置默认值;
  3. 若非设置默认值不可, 只能断言泛型😞

参考

  1. How to fix TS2322: “could be instantiated with a different subtype of constraint 'object'”?
  2. Issue a custom error message when trying to assign constraint type to generic type parameter