Reputation: 1137
function f<T>(defaultValue?: T) { return defaultValue; }
const definitelyUndefined = f<string>(); // type: string | undefined
const definitelyString = f<string>('foobar'); // type: string | undefined
Is it possible to define f()
such that definitelyUndefined
is implicitly undefined
and definitelyString
is implicitly string
?
My real world use case is a function I'm working with and want to improve. It is function f<T>(o: { [key: string]: T }, key: string, defaultValue?: T)
and it returns key[o]
if it exists, otherwise defaultValue
. When I provide it a defaultValue
, I'm guaranteed to get T
back, but Typescript considers it T | undefined
.
Upvotes: 8
Views: 5051
Reputation: 39005
The accepted answer conflates 'type space' and 'value space' (as one of the comments states).
This is suboptimal. Here is an alternative:
Typescript >= 4.7.4
// Type definition
type ValueElseUndefined<T> =
T extends (string | number | boolean | symbol | object) ? T : undefined;
// Function definition
function f<T>(defaultValue?: T): ValueElseUndefined<T> {
return defaultValue as any;
}
// Invocations
const definitelyUndefined = f(); // type: undefined
const definitelyString = f('foobar'); // type: string
Typescript < 4.7.4
// Type definition
type ValueElseUndefined<T> =
T extends (string | number | boolean | symbol | object) ? T : undefined;
// Function definition
function f<T>(defaultValue?: T): ValueElseUndefined<typeof defaultValue> {
return defaultValue as any;
}
// Invocations
const definitelyUndefined = f(); // type: undefined
const definitelyString = f('foobar'); // type: string
Upvotes: 4
Reputation: 148990
For your first case, I immediately thought of using an overload, for example:
function f<T>(): undefined;
function f<T>(value: T): T;
function f<T>(value?: T) { return value; }
const definitelyUndefined = f(); // type: undefined
const definitelyString = f('foobar'); // type: "foobar"
However, for the more complex use case, I think perhaps you can solve it with overloads and more complex generics, for example:
function f<T, K extends string & keyof T>(o: T, key: K, defaultValue?: T[K]): T[K];
function f<T, V = undefined>(o: T, key: string, defaultValue?: V): V;
function f<T, V = undefined>(o: T, key: string, defaultValue?: V) {
return o[key] || defaultValue;
}
const obj = { foo: "123" };
const definitelyUndefined = f(obj, "bar"); // type: undefined
const definitelyNumber = f(obj, "bar", 123); // type: number
const definitelyString = f(obj, "foo"); // type: string
I don't know if this will work for every conceivable scenario (because the return type is determined based on the generic type argument, not the actual function argument), but I think it gets pretty close.
Upvotes: 13