如何修复TS2322错误: "could be instantiated with a different subtype of constraint 'object'"?

245

我在递归类型中遇到了类型检查错误。

我正在尝试为react-jss样式对象编写类型。

type StylesFn<P extends object> = (
  props: P
) => CSS.Properties<JssValue<P>> | number | string;

type JssValue<P extends object> =
  | string
  | number
  | Array<string | number>
  | StylesFn<P>;

// @ts-ignore
interface StylesObject<K extends string = any, P extends object = {}>
  extends Styles {
  [x: string]: CSS.Properties<JssValue<P>> | Styles<K, P>;
}
export type Styles<K extends string = any, P extends object = {}> = {
  [x in K]: CSS.Properties<JssValue<P>> | StylesObject<any, P> | StylesFn<P>
};

它的功能很好,但 TypeScript 给出了一个错误。我使用 @ts-ignore,但这不太好。

ERROR 24:11  typecheck  Interface 'StylesObject<K, P>' incorrectly extends interface 'Styles<any, {}>'.
  Index signatures are incompatible.
    Type 'Properties<JssValue<P>> | Styles<K, P>' is not assignable to type 'StylesFn<{}> | Properties<JssValue<{}>> | StylesObject<any, {}>'.
      Type 'Properties<JssValue<P>>' is not assignable to type 'StylesFn<{}> | Properties<JssValue<{}>> | StylesObject<any, {}>'.
        Type 'Properties<JssValue<P>>' is not assignable to type 'Properties<JssValue<{}>>'.
          Type 'JssValue<P>' is not assignable to type 'JssValue<{}>'.
            Type 'StylesFn<P>' is not assignable to type 'JssValue<{}>'.
              Type 'StylesFn<P>' is not assignable to type 'StylesFn<{}>'.
                Type '{}' is not assignable to type 'P'.
                  '{}' is assignable to the constraint of type 'P', but 'P' could be instantiated with a different subtype of constraint 'object'.

这个错误是什么意思?


1
这是与此问题相同的错误消息,其中可能部分由注释回答。 - ChrisW
5个回答

485

补充@fetzz的优秀回答。


简短回答

TLDR; 这种错误信息有两个常见的原因。你正在做第一个(见下文)。除了文字之外,我会详细解释这个错误信息想要传达的内容。

原因 1: 在 TypeScript 中,不允许将具体实例分配给类型参数。以下是一个“问题”和“问题解决”的示例,您可以比较差异并查看更改:

问题

const func1 = <A extends string>(a: A = 'foo') => `hello!` // Error!

const func2 = <A extends string>(a: A) => {
    //stuff
    a = `foo`  // Error!
    //stuff
}

解决方案

const func1 = <A extends string>(a: A) => `hello!` // ok

const func2 = <A extends string>(a: A) => { //ok
    //stuff
    //stuff
}

TS Playground 中查看

原因2:虽然您的代码中没有以下错误,但这种错误消息弹出的情况也很常见。您应该避免做以下操作:
在类、类型或接口中重复(错误地)使用类型参数。

不要让下面代码的复杂性困扰你,我只希望你集中精力看如何通过去除字母'A'来解决问题:
问题:
type Foo<A> = {
    //look the above 'A' is conflicting with the below 'A'
    map: <A,B>(f: (_: A) => B) => Foo<B>
}

const makeFoo = <A>(a: A): Foo<A> => ({
   map: f => makeFoo(f(a)) //error!
})

解决方案:

type Foo<A> = {
    // conflict removed
    map: <B>(f: (_: A) => B) => Foo<B>
}

const makeFoo = <A>(a: A): Foo<A> => ({
   map: f => makeFoo(f(a)) //ok
})

TS Playground中查看:


长篇回答


了解错误信息

接下来,我将分解错误消息的每个元素:

Type '{}' is not assignable to type 'P'.
  '{}' is assignable to the constraint of type 'P', but 'P' could be
 instantiated with a different subtype of constraint'object'

{}是什么类型

{}

是一种类型,你可以分配除了null或undefined以外的任何东西。例如:
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...

查看:TS Playground


什么是“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是类型S的子类型:如果A添加细节,而不是S中删除已存在的细节。

类型A和类型B是类型S的不同子类型:如果AB都是S的子类型,但AB是不同的类型。换句话说:AB为类型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指向类型结构{ foo: 'Bar' }。 一般语法是:type [type_alias_name] = [type_structure]。 Typescript类型系统仅检查[type_structure],而不检查[type_alias_name]。这意味着在TS中,在类型检查方面以下两种情况没有区别:type A = { foo: 'bar' }type B = { foo: 'bar' }。更多信息请参见:官方文档

什么是类型约束'X'

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

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

阅读:类型约束“B”是类型“A”的约束


为什么会出现错误

为了说明问题,我将展示三种情况。每种情况唯一变化的是Type Constraint,其他都不会改变。

我希望你注意到Type ConstraintType Parameter的限制不包括不同的子类型。让我们来看看:

假设:

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

案例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

案例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 !


请在TS playground中查看


结论

以下函数:

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

因此,将具体类型分配给泛型{{Type Parameter}}是不正确的,因为在TS中,{{Type Parameter}}始终可以实例化为某个任意不同的子类型。
解决方案:
永远不要将具体类型分配给泛型类型参数,将其视为只读!而是这样做:
const func = <A extends Foo>(a: A) => `hello!` //ok!

请在TS Playground中查看


4
"<A extends Foo>(a: A) => hello!" 和 "(a: Foo) => hello!" 是等价的吗? - Trevor Karjanis
1
@TrevorKarjanis 它们有一点不同,例如:<A extends Foo>(a: A = foo) => 'hello!' 会产生编译错误,而 (a: Foo = foo) => 'hello' 是有效的代码。请参见此处。 - Flavio Vilante
1
@FlavioVilante 我上一条评论有误。正确的应该是 const func = <A extends Foo>(a: A = foo_SubType as A)。请查看这里。希望这可以帮到你! - transang
1
TypeScript 解释这个语法的方式就是无意义的,当有人写 <A extends string>(a: A = 'foo') 时,他(很可能)的意思是 a 应该有一个默认值为 "foo" 的字符串类型,这是完全可以的。 - Eliav Louski
1
@FlavioVilante,我发的错误和海报上的错误不一样,很抱歉给您带来了不便。我已经删除了评论。我的错误是关于T可能被实例化为另一个派生类,我已经用另一种方法解决了。谢谢。 - Malkaviano
显示剩余15条评论

76
那个错误警告你,你的泛型类型P不能被赋值为{},因为泛型类型P可以更具体或受限于可能与默认值冲突的特定类型。
这意味着值{}无法满足泛型类型P可以使用的所有可能类型。
让我们创建另一个只有布尔值的示例,这应该更容易理解:
interface OnlyBoolIdentityInterface<T extends boolean> {
  (arg: T): T;
}

function onlyBoolGeneric<T extends boolean>(arg: T = false): T {
  return arg;
}

如果您定义的类型比布尔型更具体,例如:
type TrueType = true;

如果您将函数OnlyBoolIdentityInterface专门支持true值,如下所示:

const onlyTrueIdentity: OnlyBoolIdentityInterface<TrueType> = onlyBoolGeneric;

即使TrueType遵守了由T extends boolean设定的约束条件,但默认值arg: T = false不是TrueType
这就是错误信息试图向您传达的情况。
那么如何修复此类错误呢?
  1. 要么删除默认值
  2. 要么T需要扩展默认参数的专门类型,即我的示例中的false
  3. 要么T可以直接干预接收默认参数的参数
有关此错误消息的更多上下文信息,请参阅建议此错误消息的问题:https://github.com/Microsoft/TypeScript/issues/29049

我已经在“type Styles”中使用“P extends object = any”替换了“P extends object = {}”,这解决了我的问题。谢谢 - teux
6
我不理解这两个答案中的任何一个。我 认为 我理解了使它不可能的机制,但我不明白为什么这会成为一个有意的警告。使用你回答中的 fnfn(true)fn(false) 都是正确的,对吗?在函数定义中的 = false 部分不是给它提供了一个默认值,所以 fn() 相当于 fn(false) 吗?为什么 obj 可能为 true 会影响我的意图,即默认参数应该是 false - T Tse
7
当您创建一个通用函数时,您允许使用该函数的人制作更专门的新版本。因此,如果我创建了一个仅接受 true 值的较新版本,则您的默认值将破坏可能的专门版本。或许我创建的另一个示例代码能够帮助您:https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgEIHt0BsCSATCcYMATwDEQAeAFQD5kBvAWAChl3kAKOKAcwC5k1AJSDqAblYBfVqxgBXEAjDB0IZMAJFSGbDWQQAHpBB4AzsgBGmLBDgha3PmOQBeZPCxmIooY1YcyFAQYPJQ6jy8kiwyLKykAA4o1FDyENQkSW7IYKkQ0awIamZgyADyIFgkKWn4hCqkFIK6uFoN5FQ16ZkQ9O6a9cQkLQUsQA。 - Fetz
@Fetz 现在这有意义了。 - T Tse
第三次阅读线程,我目前弄清楚的是:要修复这个问题,你可能需要简化代码或删除默认参数。同意将其作为错误消息可能更好。 - Thom
显示剩余2条评论

3
一份更简短的说明。
抛出错误的示例:
type ObjectWithPropType<T> = {prop: T};

// Mind return type - T
const createCustomObject = <T extends ObjectWithPropType<any>>(prop: any): T => ({ prop });

type CustomObj = ObjectWithProp<string> & { id: string };

const customObj = createCustomObj<CustomObj>('value'); // Invalid
// function will only ever return {prop: T} type.

问题在于返回对象只匹配属性prop,而不是任何其他属性。扩展ObjectWithPropType会给人一种错误的类型约束感觉。这个例子总体来说是一个错误的方法,它仅用于说明对象属性中的实际冲突。
如何在create函数中约束子类型:
type StringPropObject = ObjectWithPropType<string>

const createCustomObject = <T>(prop: T extends ObjectWithPropType<infer U> ? U : T): ObjectWithPropType<T> => ({ prop });

const stringObj = createCustomObject<StringPropObject>('test');

在这种情况下,函数要求参数为字符串。该对象仅具有prop属性,函数确实返回所需的形状。

2

补充 @flavio-vilante 的优秀回答。

如果你仍然想要执行(参数 a 可以省略,如果省略,则回退到 foo_SubType

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

可以尝试:

const func = <A extends Foo>(a?: A) => {
    const aa = a === undefined ? foo_SubType : a;
    return `hello!`;
} 

或者,您可能并不真的想要通用类型,而是直接使用Foo类型。

const func = (a: Foo = foo_SubType) => `hello!`

1
在我的情况下,我想要使用泛型枚举类型(T extends enum)来选择组件,结果是:
export function BasicSelector<T extends string>(props: IProps<T>) {

对于事件处理程序,我遇到了这个错误“类型“字符串”不能分配给类型“T””。因此,最终我强制规定了类型。

          onChange={(e) => onChange(e.target.value as T)}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接