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
.