Is it possible to make a property optional based on a generic type being undefined?
For example, say I have the following type that accepts a couple of possibly-undefined generic type parameters:
type Data<A = undefined, B = undefined> = {
a: A;
b: B;
}
I'm trying to make property a
optional (i.e. a:? A
) when the generic type A
is undefined. Likewise, property b
should be optional when the generic type B
is undefined.
Some test cases to help illustrate what I'm trying to achieve.
// Should compile
// type `A` is undefined so property "a" should be optional
const test1: Data<undefined, string> = { b: 'B' };
// Should error
// type `B` is not undefined so `b` should be required
const test2: Data<number, string> = { a: 1 };
// Should compile
// Types`A` & `B` are not undefined
// so properties `a` and `b` are required and present
const test3: Data<number, string> = { a: 1, b: 'B' };
You can write your own utility type like UndefinableToOptional<T>
which turns all properties which accept undefined
into optional properties. Note that there's no way to "conditionally" turn on and off mapping modifiers like ?
inside a single mapped type. This is requested in microsoft/TypeScript#32562. The closest we can get is to break T
into the part we want to be optional and the part we want to be required and then intersect them together:
type UndefinableToOptional<T> =
{ [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined> } &
{ [K in keyof T as undefined extends T[K] ? never : K]: T[K] }
That definition uses the filtering functionality of key remapping to act like either Pick
or Omit
.
Once we have that we can define your Data
type
type Data<A = undefined, B = undefined> =
UndefinableToOptional<{
a: A;
b: B;
}>
and test out how it behaves:
type X = Data<undefined, string>;
/* type X = {
a?: never;
} & {
b: string;
} */
type Y = Data<number, string>;
/* type Y = {} & {
a: number;
b: string;
} */
And just to check, let's throw some other types at it:
type Z = UndefinableToOptional<{
a: number,
b: string | undefined,
c?: boolean,
readonly d: Date | undefined,
readonly e: null
}>
/* type Z = {
b?: string;
c?: boolean;
readonly d?: Date;
} & {
a: number;
readonly e: null;
} */
Looks good. Intersections of object types are equivalent to single object types (and if it's important, you could rewrite UndefinableToOptional<T>
to result in a single object type... you could even make sure the property order doesn't change. But those are more complicated implementations for questionable benefit).