原文地址如何解决 TS2322: “could be instantiated with a different subtype of constraint” - 掘金

如何解决 TS2322 报错: “could be instantiated with a different subtype of constraint”

遇到问题

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

1
2
3
4
5
6
7
8
9
10
11
12
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 } , 报错消失了.

1
2
3
4
5
6
7
8
9
10
11
12
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 报错信息

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

1
2
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。例如:

1
2
3
4
5
6
7
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 什么意思?

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

1
2
3
4
5
// 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 的不同子类型
1
2
3
4
5
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" };
1
2
3
4
5
type A = number;
type B = 2;
type C = 7;
type D = number;
type E = `hello world`;
1
2
3
4
5
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’。

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

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

类型约束 extends

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

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

Given:

1
2
3
4
5
6
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: 无类型约束

1
2
3
4
5
6
7
8
9
10
11
12
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 中,类型约束不会限制其子类型.

1
2
3
4
5
6
7
8
9
10
11
12
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: 更具体的约束

1
2
3
4
5
6
7
8
9
10
11
12
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 !

总结示例

以下函数:

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

产生如下错误信息:

1
2
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’ 子类型来调用函数。例如,下面的所有函数调用都被认为是有效的:

1
2
3
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 中,类型形参总是可以实例化为任意不同的子类型。

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

相反, 这样做:

1
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