I am writing a function that returns undefined when an input is falsy. In TypeScript 5.6.2, I can't get the following example past the type checker:
// Return type: number | "" | undefined
const f1 = (s: string, n: number) => (s || undefined) && n;
// Type 'number | "" | undefined' is not assignable to type 'number | undefined'.
// Type 'string' is not assignable to type 'number'.(2322)
const x: number | undefined = f1("a", 1);
The inferred return type says that f1
could return an empty string. This is annoying because I need to handle a string return type that is impossible. I think the return type should be number | undefined
instead.
The same thing happens when the string and number are switched:
// Return type: string | 0 | undefined
const f2 = (s: string, n: number) => (n || undefined) && s;
When I use a ternary operator or coerce to boolean, the return type is inferred correctly:
// Return type: number | undefined
const f3 = (s: string, n: number) => (s ? true : undefined) && n;
// Return type: number | undefined
const f4 = (s: string, n: number) => (!!s || undefined) && n;
TypeScript's type system isn't expressive enough to represent "a truthy string
" in a way that has an effect on control flow analysis. Only the empty string of the literal type ""
is falsy, so all strings except the empty string are truthy. But in order to say "all strings except the empty string" TypeScript would need something like subtraction types as requested in microsoft/TypeScript#4183 or negated types as requested in microsoft/TypeScript#4916. It has neither of these. So there's no string ~ ""
or string & not ""
.
If there were such a type, then presumably s || undefined
would be of type (string & not "") | undefined
(since logical OR (||
) evaluates to its first operand if it is truthy, or its second operand otherwise, thus you get the truthy part of the first operand unioned with the second operand). And then (s || undefined) && n
would be of type number | ("" & not "") | undefined
which would simplify to number | undefined
(since logical AND (&&
) evaluates to its first operand if it is falsy, or its second operand otherwise, thus you get the falsy part of the first operand unioned with the second operand).
What actually happens is that s || undefined
is of the type string | undefined
, and the fact that it cannot be ""
is lost. And then (s || undefined) && number
evaluates to the falsy part of the first operand (string | undefined
) union since string | undefined
is falsy if and only if its value is "" | undefined
, that's what you get unioned with number
. TypeScript loses track of "cannot be ""
".
This also explains how 0
shows up when string
and number
are switched. 0
is the only falsy number
, but there is no number ~ 0
or number & not 0
to express truthy number
s.