I have a generic function that will return either an object of the generic type or null (and possibly undefined), so I added | null | undefined
to the return type but it doesn't seem to get applied:
const test = <T>(obj: TypeA<T>): T | undefined | null => {
return someObject[obj];
};
When I look at the generated definition of the function I get const test: <T>(obj: TypeA<T>) => T
(without null or undefined). This also means if I call it like this:
test<number>(someNullArg).toPrecision()
TypeScript doesn't complain, even though it should and even though the code throws a Cannot read properties of null
error at runtime. If I replace the generic type the return type is correctly generated (const test: (obj: TypeA<any>) => any | undefined | null
).
Update
Here's a simplified version of the actual implementation:
let value;
const get = <T>(obj: Promise<T>): T | undefined | null => {
obj.then(val => value = val);
return value;
}
It takes a Promise and returns its value (if it is resolved). It's designed to be used in a StencilJS component to output an asynchronous value in the synchronous render method. Basically I'm trying to recreate the functionality of Angular's async
pipe.
2nd Update
I've tried to debug this further and I think this might be a bug in TypeScript.
I've reduced it to a simpler example:
function test<T>(): T | undefined | null {
return (window as any).foo;
}
const x = test(); // type is "unknown"
const y = test<number>(); // type is "number"
y.toFixed(); // should throw a type error but doesn't
Apparently | undefined | null
is completely ignored once the generic type is resolved and I don't know why since the return type is quite specific.
Here's the entire source (the important part is the async
function).
CodePudding user response:
In order to deal with this properly, I'd suggest you enable the --strictNullChecks
compiler option and deal with any ensuing errors it flags. (I would go further and say to enable the entire --strict
suite of compiler features to get a "standard" amount of type safety from the language.) Indeed, the TypeScript handbook says "we always recommend people turn strictNullChecks
on if it’s practical to do so in their codebase."
With --strictNullChecks
off, the null
and undefined
types are subtypes of every other type; any type T
will accept a value of type null
or undefined
, and so forming a union of T
with null
or undefined
is equivalent to just T
. So null
and undefined
behave the same as the never
type, and are absorbed into all unions. This is known as "subtype reduction" and something TypeScript does quite a bit.
Only with --strictNullChecks
on does the compiler have any chance of tracking null
and undefined
.
You could make the argument that, if you are using the --declaration
compiler option to emit .d.ts
declaration files to be used by others including your code as a module, the emitted declarations might not be accurate if the compiler eagerly reduces T | null | undefined
to T
, since the consuming applications might have --strictNullChecks
enabled, and they would then be under the mistaken impression that get()
always returns a T
from a Promise<T>
instead of possibly undefined
or null
.
And you'd be right. This is considered a design limitation of TypeScript as described in microsoft/TypeScript#18773. As mentioned there, "by the time we're emitting the .d.ts
file, the null
ness of any given type is long gone. We'd basically have to pretend [--strictNullChecks
] was on as an entirely separate compile phase since it potentially changes any type result." Since this would cause compile time to essentially double when --strictNullChecks
is off and --declaration
is on, they don't do it.
Since they can't fix this, you really should turn on --strictNullChecks
if you're generating .d.ts
files for your code. Consumers who don't use --strictNullChecks
wouldn't care one way or the other, but those who do will appreciate it.
Anyway, though, null
and undefined
being absorbed into unions when --strictNullChecks
is disabled is working as intended and not a bug. You might want to file a feature request to do this less eagerly so that null
and undefined
types are preserved in more situations, but I wouldn't expect the TS team to implement it, even if it's not immediately declined.
An issue that's similar in spirit is microsoft/TypeScript#29729, where a union containing string
and other string literal types like "a" | "b" | string
is subtype-reduced to just string
. Some people want "a" | "b" | string
to stay as-is for autocomplete and documentation purposes, but the compiler sees "a" | "b" | string
as "a fancy way of writing string
". So I'd expect any request of the form "make T | null | undefined
stay as-is for documentation purposes" would be rejected or closed as a design limitation, with a note that T | null | undefined
is just "a fancy way of writing T
".