【问题标题】:How to fix TS2322: "could be instantiated with a different subtype of constraint 'object'"?如何修复 TS2322:“可以用不同的约束‘对象’子类型实例化”?
【发布时间】:2019-10-23 14:59:24
【问题描述】:

A 在递归类型中有类型检查错误。

我正在尝试为 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>
};

它工作正常,但打字稿写了一个错误。我用@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'.

这个错误是什么意思?

【问题讨论】:

  • 这与this question 中的错误消息相同,cmets 可能会部分回答。

标签: typescript typescript-generics


【解决方案1】:

补充@flavio-vilante 的精彩回答。

如果你还想做

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

(参数a可以省略,如果省略则回退到foo_SubType),
可以试试:

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

或者,也许您并不真正想要泛型类型,而是直接键入 Foo。

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

【讨论】:

    【解决方案2】:

    补充@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: 虽然您没有在代码中出现以下错误。弹出这种错误消息也是正常情况。你应该避免这样做:

    在类、类型或接口中重复(错误地)Type Parameter。

    不要让下面代码的复杂性让您感到困惑,我想让您集中精力的唯一一件事是删除字母“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
    
    

    什么是different subtype

    两种类型是相等的:如果它们不添加或删除彼此相关的细节。

    两种类型不同:如果它们不相等。

    A 类型是S 类型的子类型:如果A 添加细节而不从S 中删除已经存在的细节。

    A 类型和B 类型是S 类型的不同子类型:如果A 和B 是S 的子类型,但A 和@987654352 @ 是不同的类型。换句话说: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
    

    注意:Structural Type

    当您在 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' }。更多信息请见:Official Doc。


    什么是constraint of type'X'

    类型约束就是您放在“extends”关键字右侧的内容。在下面的示例中,Type Constraint 是“B”。

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

    读取:类型约束“B”是constraint of type 'A'


    为什么会发生错误

    为了说明,我将向您展示三个案例。在每种情况下唯一会有所不同的是Type Constraint,其他都不会改变。

    我想让你注意的是Type Constraint 对Type 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' }
    

    案例 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 中,Type Constraint 不限制不同的子类型

    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 可以总是被实例化为任意不同的子类型。

    解决方案:

    永远不要将具体类型分配给泛型类型参数,将其视为read-only!而是这样做:

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

    见TS Playground

    【讨论】:

    • @TrevorKarjanis 它们略有不同,例如:&lt;A extends Foo&gt;(a: A = foo) =&gt; 'hello!' 产生编译错误,而 (a: Foo = foo) =&gt; 'hello' is valid code. See here.
    • @Max 谢谢。你问:Why? SubFoo and DiffSubType both implement same prop1 and both add one additional prop, shouldn't they be both be just subtypes of Foo? 它们只是 Foo 的子类型,你没看错。问题是:鉴于它们是亚型,它们是相同的还是不同的亚型?答:不同是因为它们添加了彼此相关的不同属性/方法。 See this example here(可能你对interface关键字比较熟悉)。
    • @ford04 谢谢。我对答案进行了更新以反映您的 cmets。
    • @MrWolfZ 在表达式const f = &lt;T extends X&gt;(a: T) =&gt; //... 中,术语extends 表示要求T 必须遵守以下两个要求中的任何一个:或(1)T 等于X,要么; (2) T 等于 X 的任意子类型。人们错误地假设术语extends 只表达(1)而不表达(2)。但是因为 (2) 也是可能的,所以 TS 禁止您将类型为 X 的特定实例分配给类型 T 并希望您选择不同的方法,如果您想保持类型安全。 (所以在你的情况下,这不是对 Pick 的限制,而是对 T 本身的限制)。
    【解决方案3】:

    该错误警告您,您的通用类型 P 不能分配给 {},因为通用类型 P 可以更明确地定义或限制为可能与默认值。

    这意味着值{} 不能满足通用类型P 可以使用的所有可能类型。

    让我们再创建一个只有布尔值的示例,应该更容易理解:

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

    如果您定义了比布尔值更具体的类型,例如:

    type TrueType = true;
    

    如果你将函数 OnlyBoolIdentityInterface 专门化为只支持这样的真值:

    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 = {},这解决了我的问题。谢谢
    • 两个答案我都不明白。我认为我得到了使它不可能的机制,但我不明白为什么这将是一个有意的警告。从您的答案中使用fn,fn(true) 和fn(false) 都是正确的,对吧?函数定义中的= false 部分不是给它一个默认值,所以fn() 将等同于fn(false)?为什么obj 可能是true 会影响我将默认参数设为false 的意图?
    • @ShioT 创建通用函数时,您允许使用该函数的人制作更专业的新版本。因此,如果制作一个只接受真值的更新版本,您的默认值将破坏可能的专用版本maybe this other example code that I created will help
    • @Fetz 现在说得通了。
    • 第三次阅读线程,到目前为止我的想法:要解决此问题,您可能需要简化代码或删除默认参数。同意这可能更适合作为错误消息。
    【解决方案4】:

    稍微简短的解释。

    引发错误的示例:

    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 属性和函数确实返回所需的形状。

    【讨论】:

      猜你喜欢
      • 2021-02-13
      • 2020-03-27
      • 2021-11-24
      • 2019-12-13
      • 2021-08-14
      • 1970-01-01
      • 2021-10-14
      • 2021-06-26
      • 2020-01-22
      相关资源
      最近更新 更多