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']
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;
}
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;
}