Why does a Typescript type conditional on `T extends undefined`, with T instantiated with `boolean`, resolve T to `never`?

As written,

type OptionalArgBroken<Arg> = Arg extends undefined ?
    () => void :
    (arg: Arg) => void;

is a distributive conditional type because the type being checked, Arg, is a naked generic type parameter.

“Distributive” means that if the Arg passed in is a union, then the type will be evaluated for each member of the union separately and then united back together (so the operation is distributed across the union). In other words, OptionalArgBroken<A | B | C> will be the same as OptionalArgBroken<A> | OptionalArgBroken<B> | OptionalArgBroken<C>.

This is likely not your intent, as evidenced by the fact that you are happy with the results when you wrap your check in [] (which makes the checked type no longer “naked” by “clothing” it).


Furthermore, the TypeScript compiler treats the boolean type as a shorthand for the union of true and false, the so-called boolean literal types:

type Bool = true | false;
// type Bool = boolean

If you hover over Bool in your IDE with IntelliSense, you will see that Bool above is displayed as boolean.

This might be surprising if you think of boolean as a single type and not a union of two other types. And one place this shows up is when you pass boolean to a distributive conditional type: OptionalArgBroken<boolean> is OptionalArgBroken<true | false> which is OptionalArgBroken<true> | OptionalArgBroken<false> which is

type OABBool = OptionalArgBroken<boolean>;
// type OABBool = ((arg: false) => void) | ((arg: true) => void)

You passed in what you thought was a single type and got a union of function types out. Oops. (See microsoft/TypeScript#37279)


And a union of function types can only be safely called with an intersection of their parameters. Read the TS3.3 release notes on support for calling a union of functions for information about why this is.

But that means a value of type OptionalArgBroken<boolean> can only be called with an argument of type true & false, which is reduced to never (see microsoft/TypeScript#31838) because there is no value which is both true and false.

And therefore, when you try to call haveArgBroken, it expects the parameter passed in to be of type never:

const haveArgBroken: OptionalArgBroken<boolean> = function (b: boolean) { };
// haveArgBroken(arg: never): void

And true is not of type never, so it fails:

haveArgBroken(true); // Type error

And that’s why your original code did not work.


Note that the same thing happens with

type ExtendsUndefined<T> = T extends undefined ? 'yes' : 'no'

but it is benign because ExtendsUndefined<boolean> becomes ExtendsUndefined<true> | ExtendsUndefined<false> which is 'no' | 'no' which is reduced to just 'no'. It happens to be what you want, but only because there’s no way to distinguish the 'no' that came from true with the one that came from false.


Playground link to code

Leave a Comment