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