While working through the typescript challenges I bumped into the following snippet (simplified):
type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
This can be used like this:
type test1 = MyEqual<{ bar: number }, { readonly bar: number }>
// ^? false
type test2 = MyEqual<{ bar: number }, { bar: number }>
// ^? true
Problem is I'm struggling to understand what <T>
does in MyEqual
, how does it get its value and generally how it works.
Any clues? Thanks!
It's actually not particularly illuminating to understand what T
is doing in <T>() => T extends X ? 1 : 2
. Let's forget about that for now and come back to it later.
You're looking at a particular implementation of the so-called type-level equality operator, as discussed in How to test if two types are exactly the same. There are types in TypeScript which are mutually assignable (that is, you can assign a value of type X
to a variable of type Y
and vice versa) but which are represented differently by the compiler. For example, the intersection {a: 0} & {b: 1}
is mutually assignable to the single object type {a: 0; b: 1}
, but they are not considered "identical" according to the compiler. The goal of MyEqual<X, Y>
is to somehow get the compiler to tell you whether it considers X
and Y
to be identical and not just mutually assignable.
Most naive implementations would give you the latter, like this:
type MyEqual<X, Y> = [X] extends [Y] ? [Y] extends [X] ? true : false : false
One approach you could take is to find a "black box" type function which is so complicated that the compiler has no idea how to analyze what it does. We don't really care what it does either, just that the compiler can't do any analysis on it. So we're looking for a type function like
type BlackBox<Z> = ⋯Z⋯ // something goes here
that's so complicated that the compiler only thinks BlackBox<X> extends BlackBox<Y>
if X
and Y
are completely identical. If they're not identical then the compiler just gives up and says that BlackBox<X>
and BlackBox<Y>
are different.
Then you could almost write MyEqual
like
type MyEqual<X, Y> = BlackBox<X> extends BlackBox<Y> ? true : false;
...except that the compiler will take a shortcut since both are referring to the same BlackBox
definition. So the approach will have to be to either inline BlackBox
into MyEqual
like
type MyEqual<X, Y> = ⋯X⋯ extends ⋯Y⋯ ? true : false;
// where ⋯X⋯ is what you get if you copy `BlackBox<X>`
// definition in manually
or define two syntactically identical BlackBox
type functions. That is, we can write
type BlackBox1<Z> = ⋯Z⋯
type BlackBox2<Z> = ⋯Z⋯ // <-- identical to the above except for the name
type MyEqual<X, Y> = BlackBox1<X> extends BlackBox2<Y> ? true : false;
So now the goal is to look for an appropriate BlackBox<Z>
definition. It turns out that the following one works:
type BlackBox<Z> = <T>() => T extends Z ? 1 : 2
and therefore you could write
type BlackBoxA<Z> = <T>() => T extends Z ? 1 : 2
type BlackBoxB<Z> = <T>() => T extends z ? 1 : 2
type MyEqual<X, Y> = BlackBoxA<X> extends BlackBoxB<Y> ? true : false;
or the equivalent
type MyEqual<X, Y> = (<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
Again, we really don't care what BlackBox<Z>
does to Z
or what type it represents. It's not important for our purposes. We really only need it to be a "black box". Still, you seem to want to know, so let's look at it. But don't be surprised if it makes no sense or seems to serve no purpose. It's because to some extent it does make no sense and doesn't serve a purpose.
Okay, so given:
type BlackBox<Z> = <T>() => T extends Z ? 1 : 2
what do we get if we plug in something for Z
. Say, string
? then we get
type BlackBoxString = <T>() => T extends string ? 1 : 2
That's a generic function type which takes one type argument T
and no function arguments, and returns a value of the conditional type T extends string ? 1 : 2
. As written, T
isn't currently doing anything, because in generic functions, type parameters stay unresolved until such time as you call the function. So T
won't be anything until we have a function of this type and call it.
Let's see what should happen if we had a function of this type and called it:
declare const bbs: BlackBoxString;
const t = bbs(); // T cannot be inferred, becomes unknown
// const t: 2;
const u = bbs<string>();
// const u: 1;
const v = bbs<number>();
// const v: 2;
const w = bbs<"abc">();
// const w: 1;
const x = bbs<string | number>();
// const x: 1 | 2;
So when you call bbs()
and don't manually specify its type argument, type inference completely fails (there's nowhere for T
to be inferred), and it falls back to unknown
. So bbs()
produces a value of type unknown extends string ? 1 : 2
which is 2
because unknown
does not extend string
. If you manually specify the type arguments you get either 1
or 2
or 1 | 2
out of the function depending on whether or not the type argument is string
-ish.
This is a useless function type. Nobody would ever want a function of this type. It doesn't do anything good for anyone. And it's lucky that nobody wants a function like this, since it's completely impossible to actually implement a function of this type. At runtime there is no T
, and so bbs
would have to somehow know whether to output 1
or 2
based on information it doesn't have.
So BlackBox<Z>
represents a completely useless and almost always unimplementable generic function type. But again, we don't really care what it does.