The following code is trying to define the type of a function that's called with no arguments when its generic argument is undefined
, but with 1 argument for any other argument type. (There may well be better ways to accomplish this, which I'd love to see links to in the comments, but the question is about why Typescript works differently than I expected.)
When T extends undefined
is false, T
appears to turns into never
in the else branch, but only inside a function parameter list...
type OptionalArgBroken<Arg> = Arg extends undefined ?
() => void :
(arg: Arg) => void;
const suppressArgBroken:OptionalArgBroken<undefined> = function() { };
suppressArgBroken(); // Fine
const haveArgBroken:OptionalArgBroken<boolean> = function(b:boolean) { };
haveArgBroken(true); // Type error
As you can see on the Playground, the last line above gives the type error
Argument of type 'true' is not assignable to parameter of type 'never'.(2345)
After reading https://github.com/microsoft/TypeScript/issues/31751, I tried wrapping Arg
and undefined
in []
s, and that appears to have fixed the problem:
type OptionalArgWorks<Arg> = [Arg] extends [undefined] ?
() => void :
(arg: Arg) => void;
const suppressArgWorks:OptionalArgWorks<undefined> = function() { };
suppressArgWorks(); // Fine
const haveArgWorks:OptionalArgWorks<boolean> = function(b:boolean) { };
haveArgWorks(true); // Fine
Even though that fix worked, this is not the same problem:
type MakesSenseT = undefined extends undefined ? 'yes' : 'no'
const MakesSense:MakesSenseT = 'yes';
type ExtendsUndefined<T> = T extends undefined ? 'yes' : 'no'
const MakesSenseToo : ExtendsUndefined<undefined> = 'yes';
const MakesSenseThree : ExtendsUndefined<boolean> = 'no';
Why did my original code not work?
Typescript Playground Link for the above code
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
.
Okay, hope that helps; good luck!
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With