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

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

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'.

这个错误意味着什么?


这个错误是在警告你,你的泛型类型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函数专门设置为只支持真值,就像这样:

const onlyTrueIdentity: OnlyBoolIdentityInterface<TrueType> = onlyBoolGeneric;

即使TrueType尊重T extends boolean设置的约束,默认值arg: T = false也不是TrueType。

这就是错误试图传达给你的情况。

那么如何修复这种类型的错误呢?

或者删除默认值 或者T需要扩展默认参数的专门化类型,在我的示例中为false 或者T可以直接干扰接收默认参数的参数

有关此错误消息的详细信息,请参阅提示此错误消息的问题https://github.com/Microsoft/TypeScript/issues/29049。


@fetzz的回答很好。


简短的回答

TLDR;这类错误消息有两个常见原因。你正在做第一个(见下文)。除了文本,我还详细解释了这个错误消息要传达的内容。

原因1:在typescript中,不允许将具体实例赋给类型形参。下面是'problem'和'problem solved'的例子,你可以比较它们的区别,看看有什么变化:

问题

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游乐场

原因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游乐场


长回答


理解错误消息

下面我将分解错误消息的每个元素:

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游乐场


什么是不可分配的

赋值就是使特定类型的变量对应于特定实例。如果实例的类型不匹配,就会出现错误。例如:

// 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的不同子类型:如果A和B是类型S的子类型,但A和B是不同的类型。换句话说:A和B增加了S类型的细节,但它们没有增加相同的细节。

示例:在下面的代码中,所有下列语句都为真:

A和D是相等的类型 B是A的子类型 E不是A的子类型 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

NOTE: Structural Type When you see in TS the use of type keyword, for instance in type A = { foo: 'Bar' } you should read: Type alias A is pointing to type structure { foo: 'Bar' }. The general syntax is: type [type_alias_name] = [type_structure]. Typescript type system just checks against [type_structure] and not against the [type_alias_name]. That means that in TS there's no difference in terms of type checking between following: type A = { foo: 'bar } and type B = { foo: 'bar' }. For more see: Official Doc.


'X'类型的约束是什么

类型约束就是你放在“extends”关键字右边的东西。在下面的例子中,类型约束是'B'。

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

读取:类型约束'B'是类型'A'的约束


错误发生的原因

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

我想让您注意的是,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中,类型约束不限制不同的子类型

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游乐场看到


结论

函数如下:

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!

在TS游乐场见


简短一点的解释。

抛出错误的例子:

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');

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


补充@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!`

在我的情况下,我想做选择组件与通用类型的enum (T扩展enum),最终为

export function BasicSelector<T extends string>(props: IProps<T>) {

对于事件处理程序,我得到了这个错误“类型‘字符串’不能分配给类型‘T’。”所以最终我强制使用了这种类型

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