JHH
JHH

Reputation: 9285

Typescript conditional is null

I'd like to create a function that takes an argument that is usually an object but might be null/undefined, and conditionally return the type if it's non-null, otherwise null.

function objOrNull<T>(t: T): T extends null ? null: T {
   if (t == null) return null;
   
   return t;
}

This function signature seems correct, I can call this with objOrNull({}), objOrNull({a:42}), objOrNull(null), objOrNull(undefined) and the returned variable has the expected type. But the function body fails to compile since neither return null nor return t is assignable to the declared return type. If I declare the return type as T | null, the function compiles, but whenever I call it, the type I get back is potentially null, which is obviously not what I want - it should be possible to determine in runtime that if t is null the returned value is also null, otherwise it's not?

Short of adding as any or similar ugly type casts, is there a "correct" way to implement such a function? For a value, what is the equivalent of the conditional type extends null ?? Why doesn't == null tell the compiler that I'm in the T extends null condition?

(My original use-case was a function that takes an object or null, and if the object is null returns null, otherwise returns a variant of the object with some properties added and some removed, but reducing the question to my core question.) My original usecase

Upvotes: 3

Views: 1723

Answers (2)

jcalz
jcalz

Reputation: 327944

This is basically a current design limitation in TypeScript. The compiler does not use control flow analysis to narrow the type of unspecified generic type parameters (like T inside your function implementation), nor does it use it to narrow the type of values that depend on such type parameters (like t inside your function implementation).

This means that the compiler cannot verify that a value is assignable to a conditional type like T extends undefined ? null : T. Even if you check (typeof t === "undefined"), the control flow analysis that would normally narrow t from something concrete like string | undefined to undefined doesn't kick in for a type like T. Even if it did narrow t from T to (say) T & undefined, it doesn't narrow T itself to undefined, which is what would have to happen for the compiler to realize that null is assignable to T extends undefined ? null : T.

There's a canonical issue for this at microsoft/TypeScript#33912, asking for some way of having the compiler use control flow analysis to verify assignability of return values to generic conditional types. But I don't know if or when this will get addressed (although it's at least a good sign that it was proposed by one of the core members of the TS team).


This means that the best thing you can do for now is to use a type assertion (what you're calling an "ugly cast"):

function objOrNull<T>(t: T): T extends undefined ? null : T {
    return (typeof t === "undefined") ? null : t as any;
}

or the moral equivalent like a single call-signature overload:

function objOrNull<T>(t: T): T extends undefined ? null : T;
function objOrNull<T>(t: T): T | null {
    return (typeof t === "undefined") ? null : t;
}

That overload doesn't require you to use a type assertion, but it is unsafe in a similar way that using a type assertion. Meaning: either way should work to calm the compiler down, although it is not actually verifying safety here. That's your job since the compiler can't do it:

function badObjOrNull<T>(t: T): T extends undefined ? null : T {
    return (typeof t !== "undefined") ? null : t as any; // oops
}

or

function badObjOrNull<T>(t: T): T extends undefined ? null : T;
function badObjOrNull<T>(t: T): T | null {
    return (typeof t !== "undefined") ? null : t; // oops
}

Assuming you implement it properly, however, it should still behave as you expect from the caller's side:

const x = objOrNull(undefined); // null
console.log(x); // null

const y = objOrNull(Math.random() < 0.5 ? undefined : "hello"); // "hello" | null
console.log(y); // sometimes null, sometimes "hello"

Playground link to code

Upvotes: 4

MacD
MacD

Reputation: 426

You can use overloads to get the same result

function objOrNull(t:null|undefined):null
function objOrNull<T>(t: T):T
function objOrNull<T>(t: T|null):T|null {
   return t == null ? null : t;
}

Playground Link

Upvotes: 2

Related Questions