Mark
Mark

Reputation: 1137

Specifying the return type based on optional parameters

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?

Background

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

Answers (2)

Stephen Paul
Stephen Paul

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

p.s.w.g
p.s.w.g

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

Related Questions