-->

could be instantiated with a different subtype of

2020-01-25 01:14发布

问题:

A have a typecheck error in recursive types.

I am trying to write types for react-jss styles object.

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>
};

It works fine, but typescript writes error. I use @ts-ignore, but this is not fancy

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

What this error means?

回答1:

That error is warning, that your Generic Type P can't be assigned {}, since the Generic Type P can be a more defined (or restricted) type.

That means that the value {} will not satisfy all possible Types that can be used for the Generic Type P.

For example I can have a generic like this (that has the same error):

function fn<T extends boolean>(obj: T = false) {
}

and you can have a Type that is more specific than a boolean like this:

type TrueType = true;

and if you pass it to the Generic function fn:

const boolTrue: TrueType = true;
fn(boolTrue);

the assign to false is not respecting the TrueType even if TrueType respects the constraint of the generic T extends boolean

For more context about this error message see the issue that suggested this error message https://github.com/Microsoft/TypeScript/issues/29049.



回答2:

Complementing the great answer above.


SHORT ANSWER

TLDR; There are two common causes for this kind of error message. You are doing the first one (see bellow). Along the text I explain in rich details what this error message want to convey.

CAUSE 1: In TS it's not allowed to assign a 'concrete type' to a generic Type Parameter. To help you see what this mean in terms of code, following is an example of the 'problem' and the 'problem solved', so you can compare the difference:

PROBLEM

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

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

SOLUTION

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

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

CAUSE 2: Although you are not doing bellow error in your code. It is also a normal circunstance where this kind of error message pop up. I should avoid do this as well:

When you repeat (by mistaken) the Type Parameter in a class, type or interface.

Don't let the complexity of bellow code confuse you, the only thing I want you to concentrate is how the removing of the leter 'A' solves the problem:

PROBLEM:

interface 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!
})

SOLUTION:

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

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

LONG ANSWER


UNDERSTANDING THE ERROR MESSAGE

Bellow I'll explain in details what the error message is trying to convey. We'll decompose each element of the error message:

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'

NOTE: On future TS will change the error message from '{}' to unknown. They believe using '{}' is just confusing people.


WHAT IS TYPE {}

It just means an empty type or empty interface. In other words:

type EmptyType = {}
type NonEmptyType = { foo: 'bar' } 

interface EmptyInterface {}
interface NonEmptyInterface { foo: 'bar' }

You can construct instances of type '{}' using the same symbol '{}'. For example:

const a: EmptyType = {} 
const b: EmptyInterface = {} 
const c: {} = {}

WHAT IS is not assignable

To assign is to make a variable of a particular type correspond to a particular instance. If you mismatch the type of the instance you are trying to assign then you get an error. Ex:

// 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


WHAT IS A different subtype

Here's the definitions:

Two types are equals: if they have exactly the same properties and methods, no more and no less.

Two types are different: if they are not equals.

Type A is a subtype of type B: if type A is equal to type B but also it adds more information (includes more properties and/or methods).

Note that the concepts overlap. Two types can be a subtype of a particular type, and at same time be considered different from each other (different subtype).

Here is an example:

// An arbitrary type
type Foo = {
    readonly prop1: `bar`
}

// A subtype of 'Foo'
type SubType = {
    readonly prop1: `bar`
    readonly prop2: `thux` //new property introduced
}

// A different subtype of 'Foo' (but still a subtype)
type DiffSubType = {
    readonly prop1: `bar`
    readonly prop3: `different` //new property introduced, but different of property 'prop2' introduced up-above
}

Note that we can create virtually infinitely many subtypes of a particular type. This is the core point of the compiler message as you'll see below.


What is a constraint of type 'X'

The Type Constraint is simple what you put on right-side of the 'extends' keyword. In below example the Type Constraint is 'B'.

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

WHY THE ERROR HAPPENS

To ilustrate I'll show you three cases. The only thing that will vary in each case is the Type Constraint, nothing else will change.

What I want you to notice is that the restriction that Type Constraint imposes to Type Parameter only includes different types, it does not include different subtypes. Let's see it:

Given this instances:

const foo: Foo = {...} // definition omited for brevity
const foo_SubType: SubType = {...}
const foo_DiffSubType: DiffSubType = {...}

CASE 1: NO RESTRICTION

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: SOME RESTRICTION

Note below that restriction does not affect subtypes.

VERY IMPORTANT: In Typescript the Type Constraint only restricts different types, it does not restrict different subtypes

const f = <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: MORE CONSTRAINED

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) //ok <-- NO MORE ALLOWED !


CONCLUSION

Assigning a concrete type to a generic Type Parameter is incorrect because the Type Parameter can always be instantiated to some arbitrary different subtype:

Code:

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

Yields this error message:

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)

Interpretation:

In function 'func', Type Parameter 'A' can accept not only the Type Constraint 'Foo', but also all its possible subtypes (which are an infinite number of them).

When you say you want to default Type Parameter 'A' to a particular subtype of type 'Foo' (in above example doing A = foo_SubType) then I must alert you, that Type Parameter 'A' can accept many different subtypes of Type Constraint 'Foo', and not only the particular subtype you specified.

Solution:

Never assign a concrete type to a generic type parameter! Instead, do this:

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


标签: typescript