Home > front end >  Generic function getting optional property value with default value
Generic function getting optional property value with default value

Time:01-26

There is an object type like below X. Note that all properties are optional, their types are different, and for some properties, null is a valid value.

type X = {
  a?: number | null;
  b?: string;
  c?: boolean | null;
};

Now, for convenience, I want to make a function to get one of the X's property values. The function can accept a default value and return it if the property is missing.

The function I wrote is below:

function get<K extends keyof X>(k: K, x: X, defaultValue: Required<X>[K]): Required<X>[K] {
  const v = x[k];
  return v !== undefined ? v : defaultValue;
}

Because the properties are optional, I used Required<X>[K] to get the valid property type (excluding undefined).

My expected results are:

// a: number | null = 12345
const a = get('a', {}, 12345);
// b: string = 'bar'
const b = get('b', { b: 'bar' }, '?');
// c: boolean | null = null
const c = get('c', {}, null);

But I got the following compiler error at the return statement of the get function.

Type 'Required<X>[K] | (X[K] & ({} | null))' is not assignable to type 'Required<X>[K]'.
  Type 'X[K] & null' is not assignable to type 'Required<X>[K]'.
    Type 'X[K] & null' is not assignable to type 'never'.

I guess there is a mistake in the function (probably around the use of Required), but I can't solve it.

Thank you.

CodePudding user response:

When you are testing for v !== undefined TypeScript will narrow the type of v to X[K] & ({} | null). This intersection type, excludes undefined from the value type using an intersection.

Required<X>[K] does a similar thing, but there is actually a corner case where it does not remove undefined from K, when the property type includes undefined, but the property is not optional:

//number | undefined
type XX = Required<{ o: undefined | number }>['o']

Playground Link

Even though Required<X>[K] is more permissive than X[K] & ({} | null) (allowing X[K] - optional undefined) TypeScript will not be able to follow the mapped type to arrive at this conclusion.

The simplest solution would be to define a NotUndefined undefined that is similar to how TypeScript excludes undefined from v that we can use for both the return value and for the defautlValue parameter:

type NotUndefined<T> = T & ({} | null)

function get<K extends keyof X>(k: K, x: X, defaultValue: NotUndefined<X[K]>): NotUndefined<X[K]>  {
  const v = x[k];
  return v !== undefined ? v : defaultValue;
}

Playground Link

The version above does not allow undefined for the default value if the property was required and it had undefined explicitly in it's type (ex { d: boolean | null | undefined; }).

You could preserve the original behavior, by reverting defaultValue back to Required<X>[K] and adding it in a union with NotUndefined<X[K]> to the return type:

function get<K extends keyof X>(k: K, x: X, defaultValue: Required<X>[K]): Required<X>[K] | NotUndefined<X[K]>  {
  const v = x[k];
  return v !== undefined ? v : defaultValue;
}

Playground Link

  • Related