Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conditional type doesn't recognize that all inputs result in same conditional result

This example doesn't typecheck:

type Subset1 = "one" | "two";
type Subset2 = "three" | "four";
type All = Subset1 | Subset2;

type Other = {
    "one": number,
    "two": string,
    "three": boolean,
    "four": object,
};

type Extra<V> = V extends Subset1 ? string : undefined;

function doOtherThing(stuff: string){}

function doThing<V extends All>(value: V, params: Other[V], extra: Extra<V>) { }

function doSubset1Thing<V extends Subset1>(value: V, params: Other[V], extra: string) {
    doThing(value, params, extra);

    doOtherThing(extra);
}

function doSubset2Thing<V extends Subset2>(value: V, params: Other[V]) {
    doThing(value, params, undefined);
}

(TS Playground)

The error is because extra is hardcoded string in doSubset1Thing, but logically it is always a string because value is limited to Subset1, and Extra<Subset1> properly resolves to string, but for some reason, the call to doThing does not recognize that.

Similarly, inverting it for doSubset2Thing errors even though the third param will always be undefined.

For the second one, I can sort of see issues if Subset1 and Subset2 overlapped, but they don't, so I'd have assumed that TS would flatted that all out to undefined for doSubset2Thing.

Is there any way to make this work? Alternatively, am I missing something that actually does make this invalid?

like image 837
loganfsmyth Avatar asked Jan 18 '21 23:01

loganfsmyth


1 Answers

As far as I can tell, this is an instance where your code is logically correct and type-safe, but Typescript just isn't able to prove it because it lacks a rule that would be able to prove it. A simple rule like "V must extend Subset1 because that's its upper bound" would be good enough, but apparently Typescript isn't (currently) programmed to use such a rule.

One fix, which may make more sense for your use-case than a conditional type anyway, is to use function overloads: this also saves you from having to pass an explicit undefined in the second case.

function doThing<V extends Subset1>(value: V, params: Other[V], extra: string): void;
function doThing<V extends Subset2>(value: V, params: Other[V]): void;
function doThing<V extends All>(value: V, params: Other[V], extra?: string): void {
    // ...
}

Playground Link

like image 59
kaya3 Avatar answered Sep 23 '22 23:09

kaya3