Complementing @fetzz great 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
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'
{}
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
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
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:
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 intype A = { foo: 'Bar' }
you should read: Type aliasA
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 }
andtype B = { foo: 'bar' }
. For more see: Official Doc.
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'
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
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
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> {
(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?
For more context about this error message see the issue that suggested this error message https://github.com/Microsoft/TypeScript/issues/29049.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With