typescriptgenericsoptional-parameters

Make property optional if generic type is undefined


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

Solution

  • 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).

    Playground link to code