Home > Back-end >  Cannot declare generic function possibly returns null
Cannot declare generic function possibly returns null

Time:11-28

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 nullness 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".

  • Related