The issue at hand is quite clear.
Type '{ kind: "a"; }' cannot be assigned to type 'T'.
'{ kind: "a"; }' can be assigned to the constraint of type 'T',
but 'T' may be instantiated with a different subtype of constraint 'AllKinds'.
This complication arises even with a simpler type restriction in place.
function foo<T extends string>(): T {
return 'foo';
}
As evidenced by the error message below.
Type 'string' cannot be assigned to type 'T'.
'string' can be assigned to the constraint of type 'T',
but 'T' could be instantiated with a different subtype of constraint 'string'.
The root cause lies in declaring a return value as type T
, when T
does not match the type string
. While T
extends string
, this simply means that T
is a subtype of string
. For instance, the type 'bar'
is a subtype of string
, therefore we can assign 'bar'
to T
. Consequently, we anticipate the returned value to be 'bar'
rather than 'foo'
.
The solution involves abstaining from using generics altogether. If the objective is to return a string
, it suffices to explicitly state the return type as string
, without introducing the notion of returning a value of subtype T
of string
.
function foo(): string {
return 'foo';
}
Likewise, if the intention is to return a value of type AllKinds
, the definition should directly indicate returning a value of type AllKinds
, avoiding implications of returning a value belonging to a subtype T
of AllKinds
.
type KindA = {kind:'a'};
type KindB = {kind:'b'};
type KindC = {kind:'c'};
type AllKinds = KindA | KindB | KindC;
function create(kind:AllKinds['kind']): AllKinds {
switch(kind) {
case "a": return {kind:'a'};
case "b": return {kind:'b'};
case "c": return {kind:'c'};
}
}
create("a");
Edit: Achieving your desired outcome necessitates dependent types, which TypeScript does not support. However, you can enhance type safety through crafting a custom fold function.
type Kind = 'a' | 'b' | 'c';
type KindA = { kind: 'a' };
type KindB = { kind: 'b' };
type KindC = { kind: 'c' };
type AllKinds = KindA | KindB | KindC;
function foldKind<A, B, C>(a: A, b: B, c: C): (kind: Kind) => A | B | C {
return function (kind: Kind): A | B | C {
switch (kind) {
case 'a': return a;
case 'b': return b;
case 'c': return c;
}
}
}
const create: (kind: Kind) => AllKinds = foldKind<KindA, KindB, KindC>(
{ kind: 'a' },
{ kind: 'b' },
{ kind: 'c' }
);
With this approach, only a KindA
value corresponds to 'a'
, a KindB
value matches 'b'
, and a KindC
value aligns with 'c'
. Feel free to explore the demo for further clarity.