Restrict generic type T to not being undefined in TypeScript?

Viewed 9826

I defined the get(o, key) function that should work with any Object that satisfy the { get: (key: K) => R } interface.

Additionally I would want to restrict the result R to not being undefined. Is that possible?

How to to change the example below so that it won't compile, because of method Params::get(key: string): number | undefined that returns undefined?

function get<K, R>(o: { get: (key: K) => R }, key: K): R {
  return o.get(key);
}

class Params {
  constructor(public values: { [key: string]: number }) { }

  get(key: string): number | undefined {
    return this.values[key]
  }
}
const params = new Params({ a: 10 });

console.log(get(params, "a"))
2 Answers

You can do it with NonNullable<T>, but not as a constraint on the type-parameters, but using it as an interface-type on get's R, like so:

function get<K, R>( o: { get: (key: K) => NonNullable<R> }, key: K ): NonNullable<R> {
  return o.get(key);
}

So this code gives me an error:

const params = new Params({ a: 10 });

console.log(get(params, "a"))

Argument of type 'Params' is not assignable to parameter of type '{ get: (key: "a") => number; }'.
The types returned by 'get(...)' are incompatible between these types.
Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.(2345)

The catch is that NonNullable<T> prohibits both undefined and null - which may or may not be desirable in your application.

Update

I found the definition of NonNullable<T> in lib.es6.d.ts:

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

This can be tweaked to restrict only undefined:

type NonUndefined<T> = T extends undefined ? never : T;

and if I change get to this:

type NonUndefined<T> = T extends undefined ? never : T;

function get<K, R>(o: { get: (key: K) => NonUndefined<R> }, key: K): NonUndefined<R> {
  return o.get(key);
}

Then this works exactly as you've requested: it allows class Params's get to return number | null but not number | undefined.

There are no subtraction/negated types in TypeScript, but you can express "not undefined" as "{} | null" instead. Any non-null and non-undefined value is assignable to the empty type {}, and if you want to accept null and not undefined you can use {} | null:

function get<K, R extends {} | null>(o: { get: (key: K) => R }, key: K): R {
    return o.get(key);
}

This works the way you expect:

console.log(get(params, "a")); // error!
// -----------> ~~~~~~
// Type 'number | undefined' is not assignable to type '{} | null'

while a similar type that returns just number instead of number | undefined will not be an error:

const numberGetter = { get(key: string) { return 123 } };
console.log(get(numberGetter, "b")); // okay

Hope that helps; good luck!

Playground link to code

Related