Challenge 1

For reasons that are not quite clear to me, the first typescript challenge is given the number 2.

Here’s the challenge, per the github repo:

Implement the built-in ReturnType<T> generic without using it.

Example code of calling the type is:

  const fn = (v: boolean) => {
    if (v)
        return 1
    else
        return 2
}

type a = MyReturnType<typeof fn> // should be "1 | 2"

In principle this might seem tough; how do we know the return type without knowing beforehand or specifying it? On the other hand, it seems tractable because we can see that fn can only return 1 or 2 and we can know this statically (without running the code). In fact, Typescript is able to infer the return type correctly. Try it out in the Typescript playground:

const fn = (flag: boolean) => {
    if (flag) {
        return 1;
    }
    return 2;
}

type B = 1 | 2;

const b: B = fn(false);

If you change type B at all (for instance 2 | 3) then the code will fail to compile because the types no longer match.

So, if Typescript can know the answer we might be able to work it out too. With many of these challenges the solution appears to rest on using conditional types. The extends keyword that you might use to extend an interface is overloaded with the ability to be used conditionally. The Typescript docs use the following example to illustrate what this means:

type Example1 = Dog extends Animal ? number : string;

In the example we have a type that is either a string or a number depending on whether Dog is a subtype of Animal. Procedurally this would look something like: if (Dog instanceof Animal) { return number } else { return string }.

From the example given in the challenge we can see that MyReturnType takes a generic and we can use this with a conditional type, like:

type MyReturnType<T> = T extends (...args: unknown[]) => unknown ? any : never;

Obviously, this is not a useful type, but it shows that we can ensure that T is a function (never represents the empty set and will never match a real value so providing anything other than a function will cause a type error). Perhaps we can use an extreme nested conditional to work out the return type?

type MyReturnType<T> = T extends (...args: unknown[]) => string
    ? string
    : T extends (...args: unknown[]) => number ? number : never;

This would, in fact, work for functions returning a string or a number but it would be extremely hard to read and maintain. Luckily, Typescript provides the infer keyword which allows a temporary generic type to be created and used in a conditional type. We can write this:

T extends (...args: unknown[]) => infer RETURNVAL

This will create a generic type RETURNVAL that we can use later in the branches of the conditional. We can then use this just as we used string and number earlier.

Thus our eventual solution is:

type MyReturnType<T> = 
    T extends (...args: unknown[]) => 
        infer RETURNVAL ? RETURNVAL : never;

Blog

Miscellaneous thoughts.