Reputation: 9285
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
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"
Upvotes: 4
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;
}
Upvotes: 2