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;