Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand the limits of 'T extends infer U'

I've understood that something like:

type GenericExample<T> = T extends (infer U) ? U : 'bar';

is equal to:

type GenericExample<T> = T extends T ? T : 'bar';

But when stuff becomes more elaborate, TypeScript complains:

type Types = 'text' | 'date' | 'articles' | 'params';

type MyExperiment<Type extends Types> =  { t : Type };

type MyExperimentsUnion = Types extends (infer U) ? MyExperiment<U> : never;
// Type 'U' does not satisfy the constraint 'Types'.
// Type 'U' is not assignable to type '"params"'.

So I'd like to ask why this is wrong: in this particular case distribution over union should take place, so the inferred U type should be text, then date and so on. So, what does T extends (infer U) really mean and when it would be appropriate to use it?

like image 456
Andrea Simone Costa Avatar asked Apr 06 '20 08:04

Andrea Simone Costa


2 Answers

I don't think it was meant to be used the way you are using it - basically infer should be used in order to "infer" (or to resolve maybe better naming?) type, most commonly from a generic.

The way you are using it, you are creating a type that doesn't have any "dynamic" part (basically it's not generic), meaning it's always the same, and therefore inferring from something that is always the same doesn't make sense. Because at compile time you already know that Types extends only Types & '...anything else', and since you can't define that other part in your MyExperimentsUnion type, infer doesn't have much of a use.

Example usage

interface Action<T> {
    payload: T
}

type ExtractGeneric<T> = T extends Action<infer X> ? X : never

function getPayload<T extends Action<any>>(action: T): ExtractGeneric<T> {
    return action.payload;
}

const myAction = { payload: 'Test' };
const myPayloadWithResolvedType = getPayload(myAction);

In the example above myPayloadWithResolvedType would have string as resolved type, because if you weren't using infer, you'd have to pass that return type as second parameter probably like this:

function getPayloadNonExtract<T extends Action<U>, U>(action: T): U {
    return action.payload;
}

Here is the link to the playground.

Cheers.

like image 80
zhuber Avatar answered Sep 23 '22 23:09

zhuber


I know that this was asked 1 year ago, but for anyone who is late to the party, here it is.

I can't figure out how I missed this since it's right there in the docs, but as all of us here are in the same boat, let's see what's going on (starting from your code).

type Types = 'text' | 'date' | 'articles' | 'params';

type MyExperiment<Type extends Types> =  { t : Type };

// We can drop the parentheses around "infer U" as they do nothing here
type MyExperimentsUnion = Types extends infer U ? MyExperiment<U> : never;
// Type 'U' does not satisfy the constraint 'Types'.

In your code, you only capture Types using the infer keyword, but that doesn't tell typescript anything about U, so even though you will see this:

type foo = Types extends infer U ? U : never;
// foo will show up as 'text' | 'date' | 'articles' | 'params'

And wonder "Well, isn't U then of type 'text' | 'date' | 'articles' | 'params'? Why is it not assignable to MyExperiment<>?". My guess* is that the union type is only resolved at the end of the conditional, so it is not technically available when you assign it as a type parameter to MyExperiment<>.

If you wanted to do this with infer and distribute the type, you would have to add an extra condition, to constrain U at that time, to ensure that it is available as the correct type when you use it as a type parameter in MyExperiment<>.

type MyExperimentsUnion =
    Types extends infer U ?
        U extends Types ?
            MyExperiment<U>
        : never
    : never;
// MyExperiment<"text"> | MyExperiment<"date"> | MyExperiment<"articles"> | MyExperiment<"params">

Your example, however, could be also done like this

type MyExperimentsUnion<T extends Types = Types> = T extends any ? MyExperiment<T> : never;
// When you use MyExperimentsUnion with no type parameter, it will be
// MyExperiment<"text"> | MyExperiment<"date"> | MyExperiment<"articles"> | MyExperiment<"params">

* I am explicitly stating that it is my guess because I haven't really studied how TypeScript evaluates this.

like image 25
Gârleanu Alexandru-Ștefan Avatar answered Sep 23 '22 23:09

Gârleanu Alexandru-Ștefan