I wonder why TypeScript resolves conditional type to never
if I add a constraint that should guaranty desire type.
Example
Let's say we have written such code
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;
type ConditionalType<T> = T extends PrimitiveDataType
? (v: T) => void
: never;
abstract class AbstractClass<T> {
abstract value: T;
protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}
class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
value: T;
constructor(value: T) {
super();
this.value = value;
}
someMethod() {
for (const someFn of this.conditionalFunctions.keys()) {
someFn(this.value);
}
}
}
In the above code I have created a PrimitiveDataType
which is a union of all primitive data types in JavaScript.
Then I have created a ConditionalType<T>
that will resolve to some callback only if T
is one of PrimitiveDataType
.
Then I have created an abstract generic class that have a field which type(ConditionalType<T>
) depends on generic value of this class.
In the end, I have crated a SomeClass
that extends AbstractClass<T>
and add constraints that generic parameter T
have to extend PrimitiveDataType
and I get this error:
TS2345: Argument of type 'PrimitiveDataType' is not assignable to parameter of type 'never'.
Type 'undefined' is not assignable to type 'never'.
Conclusion
I thought if in SomeClass
T
have a constraint that it have to be one on PrimitiveDataType
then TypeScript will resolve conditionalFunctions
field to be a type of Map<(v: T) => void, number | undefined>
. To my surprise TypeScript resolves this type to Map<(v: never) => void, number | undefined>
what is not clear for me and I do not know where is a mistake in the way how do I think about this?
Can you explain to me why this work like that, or maybe it is bug in TypeScript compiler?
Observations
If I leave only one, type in PrimitiveDataType
then everything works okay but for more than one I am getting an error
Edit 1
If my conditional type has more than two possible return types, then distributive [T]
doesn't work and gives such error:
Expected 2 arguments, but got 1.
Looks like now TS resolves this type to type mapped for Array. I totally don't understand it now
Example
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;
type ConditionalType<T> = [T] extends [PrimitiveDataType]
? (v: T) => void
: T extends Array<unknown>
? (v: T, t: number) => void
: never;
abstract class AbstractClass<T> {
abstract value: T;
protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}
class SomeClass<T extends PrimitiveDataType> extends AbstractClass<T> {
value: T;
constructor(value: T) {
super();
this.value = value;
}
someMethod() {
for (const someFn of this.conditionalFunctions.keys()) {
someFn(this.value);
}
}
}
In my opinion, this should work the same as in this example:
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;
type ConditionalType<T> = [T] extends [PrimitiveDataType]
? (v: T) => void
: T extends Array<unknown>
? (v: T, t: number) => void
: never;
const x: PrimitiveDataType = 12;
const y: ConditionalType<typeof x> = (param: number) => undefined;
There is no problem and ConditionalType<typeof x>
is resolved to (v: number) => void
so what I would expect.
CodePudding user response:
Your generic type ConditionalType<T>
produces a distributive type when given a union.
For example when given the union string | number
it results in the following type
type Foo = ConditionalType<string | number>
// ((v: string) => void) | ((v: number) => void)
Wrap either side of the condition in square brackets to avoid this
type ConditionalType<T> = [T] extends [PrimitiveDataType]
? (v: T) => void
: never;
The above now generates the following, non-distributive, type which should work for your abstract class
type Foo = ConditionalType<string | number>
// (v: string | number) => void
CodePudding user response:
I'm quite junior on typescript, I've been using it for more or less a year and a half, but I'll still try to help you and give you a possible explanation to the error you encountered (at least what I think is the explanation), and possible ways to fix it.
/* =============== EXPLANATION =============== */
One reason, and the one that makes the most sense to me here, could be that the type narrowing wasn't sufficient. Why?
In the code you provided you defined the type ConditionalType<T>
to be a function that returns void
when T
is a PrimitiveDataType
and never
when T
is not a PrimitiveDataType
. This is logically correct, however the way you are using this function type could be not the best. This function type purpose is to narrow the type
of T
type and should be used as a type guard
. When you declare the class SomeClass
however the T
type is extending the raw PrimitiveDataType
and not the narrowed
type, so in the for
loop you are checking if a raw type PrimitiveDataType
is a valid key
of the narrowed
type version (as you declared in the AbstractClass
), but of course never
does not exists on the PrimitiveDataType
. That's why you should extend T
in the class SomeClass
declaration as shown in the second fix i provided you below. In that way T
will also inlcude never
by default. And so value
type will be the narrowed
version type aka ConditionalType<PrimitiveDataType>
and not PrimitiveDataType
.
I hope you understood my point (my english still not very good haha).
/* =============== FIXES =============== */
I'm gonna provide you two ways to fix this, with the second one being the most type-safe one.
I think you could fix this in two ways:
The first would be defining the PrimitiveDataType
to always be a function type:
type ConditionalType<T> = (v: T) => void;
Of course in the case above maybe ConditionalType
is not the right name anymore find a better one. Link to the typescript playground for the case described above -> Typescript playground
/* =============== OR =============== */
Or you can use this other approach where the type parameter T
is constrained to be a subtype of the ConditionalType<PrimitiveDataType>
type.
In this case T
extends ConditionalType<PrimitiveDataType>
which means that T
must be a function type
that takes an argument of type PrimitiveDataType
and returns void
.
type PrimitiveDataType = string | number | bigint | boolean | symbol | undefined | null;
type ConditionalType<T> = T extends PrimitiveDataType
? (v: T) => void
: never;
abstract class AbstractClass<T> {
abstract value: T;
protected conditionalFunctions: Map<ConditionalType<T>, number | undefined> = new Map();
}
class SomeClass<T extends ConditionalType<PrimitiveDataType>> extends AbstractClass<T> {
value: T;
constructor(value: T) {
super();
this.value = value;
}
someMethod() {
for (const someFn of this.conditionalFunctions.keys()) {
someFn(this.value);
}
}
}
Link to the typescript playground for the second approach -> Typescript playground
/* =============== EDIT 1 =============== */
What about the EDIT 1
The explanation for the error you are encountering when trying to use more than one parameters is pretty much the same, its always related to type narrowing, but the fix is very very simple. You just forgot to make the second parameter optional.
/* =============== FIX EDIT 1 =============== */
type ConditionalType<T> = T extends PrimitiveDataType
? (v: T) => void
: T extends Array<unknown>
? (v: T, t?: number) => void
: never;
Here there is the typescript playground with the full code for the fix of EDIT 1 -> Typescript playground
Please let me know if this was helpful. :)