240

A have a type-check 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 an 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 does this error mean?

teux
  • 2,510
  • 2
  • 9
  • 5
  • 1
    It's the same error message as in [this question](https://stackoverflow.com/q/56675333/49942) which might be partially answered by comments. – ChrisW Jun 20 '19 at 21:10

5 Answers5

476

Complementing @fetzz great answer.


SHORT ANSWER

TLDR; There are two common causes for this kind of error message. You are doing the first one (see below). Along with the text, I explain in rich detail what this error message wants to convey.

CAUSE 1: In typescript, a concrete instance is not allowed to be assigned to a type parameter. Following you can see an example of the 'problem' and the 'problem solved', so you can compare the difference and see what changes:

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
}

See in: TS Playground

CAUSE 2: Although you are not doing the below error in your code. It is also a normal circumstance where this kind of error message pops up. You should avoid doing this:

Repeat (by mistaken) the Type Parameter in a class, type, or interface.

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

PROBLEM:

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

SOLUTION:

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

See in: TS Playground


LONG ANSWER


UNDERSTANDING THE ERROR MESSAGE

Following I'll decompose each element of the error message below:

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 IS TYPE {}

It's a type that you can assign anything except null or undefined. For example:

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

See in: TS Playground


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 get an error. For example:

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

Two types are equals: if they do not add or remove details in relation to each other.

Two types are different: if they are not equal.

Type A is a subtype of type S: if A adds detail without removing already existent detail from S.

type A and type B are different subtypes of type S: If A and B are subtypes of S, but A and B are different types. Said in other words: A and B adds detail to the type S, but they do not add the same detail.

Example: In the code below, all the following statements are true:

  1. A and D are equal types
  2. B is subtype of A
  3. E is not subtype of A
  4. B and C are different subtype of 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.


WHAT IS constraint of type 'X'

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

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

Reads: Type Constraint 'B' is the constraint of type 'A'


WHY THE ERROR HAPPENS

To illustrate 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 does not include different subtypes. Let's see it:

Given:

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: 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 does not restrict different subtypes

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

See in TS playground


CONCLUSION

The function below:

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

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)

Because Typescript infers A from the function call, but there's no restriction in the language limiting you to call the function with different subtypes of 'Foo'. For instance, all function's call below are considered valid:

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

Therefore assigning a concrete type to a generic Type Parameter is incorrect because in TS the Type Parameter can always be instantiated to some arbitrary different subtype.

Solution:

Never assign a concrete type to a generic type parameter, consider it as read-only! Instead, do this:

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

See in TS Playground

Flavio Vilante
  • 5,131
  • 1
  • 11
  • 15
74

That error is warning you, that your Generic Type P can't be assigned to {}, since the Generic Type P can be a more defined, or restricted, to a particular type that can conflict with the default value.

That means that the value {} can't satisfy all the possible Types that can be used by the Generic Type P.

Let's create another example with only booleans that should be easier to understand:

interface OnlyBoolIdentityInterface<T extends boolean> {
  (arg: T): T;
}

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

if you define a Type that is more specific than a boolean for example:

type TrueType = true;

and if you specialised the function OnlyBoolIdentityInterface to only support true values like this:

const onlyTrueIdentity: OnlyBoolIdentityInterface<TrueType> = onlyBoolGeneric;

even if TrueType respects the constraint set by T extends boolean the default value arg: T = false is not a TrueType.

This is the situation is what the error is trying to convey to you.

So how can you fix this type of errors?

  1. Or you remove the default value
  2. Or T needs to extend the specialised type of the default param that on my example is false
  3. Or T can interfere directly with params that receive default params

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

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52
Fetz
  • 1,196
  • 8
  • 11
  • I'v replaced `P extends object = {}` with `P extends object = any` in `type Styles` and this resolves my issue. Thx – teux Jun 22 '19 at 15:03
  • 5
    I don't understand either of the answers. I *think* I get the mechanics that makes it impossible, but I don't get *why* this would be an intended warning. Using `fn` from your answer, both `fn(true)` and `fn(false)` are correct, right? Isn't the `= false` part in the function definition giving it a default value, so `fn()` would be equivalent to `fn(false)`? Why would `obj` possibly being `true` affect my intention that the default parameter shall be `false`? – T Tse Mar 20 '20 at 15:08
  • 6
    @ShioT when you create a Generic function you allow someone using the function to make a new version of it more specialised. So if a make a newer version that only accepts true values your default value will break that possible specialised version [maybe this other example code that I created will help](https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgEIHt0BsCSATCcYMATwDEQAeAFQD5kBvAWAChl3kAKOKAcwC5k1AJSDqAblYBfVqxgBXEAjDB0IZMAJFSGbDWQQAHpBB4AzsgBGmLBDgha3PmOQBeZPCxmIooY1YcyFAQYPJQ6jy8kiwyLKykAA4o1FDyENQkSW7IYKkQ0awIamZgyADyIFgkKWn4hCqkFIK6uFoN5FQ16ZkQ9O6a9cQkLQUsQA) – Fetz May 06 '20 at 11:33
  • @Fetz This makes sense now. – T Tse May 07 '20 at 02:54
  • Reading thread for the third time, what I figured so far: To fix this you probably need to simplify code or remove default params. Agree this is probably better intended as an error message. – Thom Jun 09 '20 at 16:24
  • Yes you are correct Thomazella. Because the Generic type can potentially conflict with the default params the best option is most likely not to use the default param since the obj is both affected by T and the default param. – Fetz Jun 11 '20 at 12:28
  • I'm not able to understand solutions 2 and 3. Can you please update your answer to include examples for the solutions to 2 and 3? – c10b10 Aug 17 '23 at 19:22
3

A bit shorter explanation.

Example that throws error:

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.

The problem here is that the return object will only ever match the attribute prop and not any other attribute. Extending the ObjectWithPropType gives a false sense of type constraint. This example is all in all a wrong approach it was used just for illustration to show actual conflict in object attributes.

How to constrain subtype in create function:

type StringPropObject = ObjectWithPropType<string>

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

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

In this case, the function requires the argument to be a string. The object only has prop attribute and function do return required shape.

Mr Br
  • 3,831
  • 1
  • 20
  • 24
2

Complementing to @flavio-vilante's great answer.

If you still want to do (the argument a can be omitted and if it's omitted, fallback to foo_SubType)

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

can try:

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

Or, maybe you don't really want generic type but just type Foo directly instead.

const func = (a: Foo = foo_SubType) => `hello!`
Loi Nguyen Huynh
  • 8,492
  • 2
  • 29
  • 52
1

In my case I wanted todo select component with generic type of enum (T extends enum) which end up as

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

and for event hanlder I got this error "Type 'string' is not assignable to type 'T'.". So eventually I enforced the type

          onChange={(e) => onChange(e.target.value as T)}
Konrad Grzyb
  • 1,561
  • 16
  • 12