Search code examples
typescriptmapped-typesconditional-types

Typescript deep replace multiple types


I use mongodb with @types/mongodb. This gives me a nice FilterQuery interface for my mogodb queries for a collection of shaped documents. In my domain object class I have some extra logic like converting dates to moment objects or floats to BigNumber objects.

For my queries I need to convert these back, so for example a Moment object needs to be converted to a date object and so on. To avoid duplication and maintaining a separate interface (just for the queries) I thought of using mapped types to replace all types of Moment to type of Date

type DeepReplace<T, Conditon, Replacement> = {
  [P in keyof T]: T[P] extends Conditon
    ? Replacement
    : T[P] extends object
    ? DeepReplace<T[P], Conditon, Replacement>
    : T[P];
};


class MyDoaminClass {
  date: Moment;
  nested: {
    date: Moment;
  };
}

const query: DeepReplace<MyDoaminClass, Moment, Date> = {
  date: moment().toDate(),
  nested: {
    date: moment().toDate()
  }
};

This basically works, but I have about 4-5 of these types that I would need to replace. Is there an elegant way to chain several DeepReplace Types or even better: Specify all type replacements in one place? I would like to avoid something like type ReplaceHell = DeepReplace<DeepReplace<DeepReplace<MyDoaminClass, Moment, Date>, BigNumber, number>, Something, string>


Solution

  • Assuming you want to do the replacement "all at once" and not as a "chain" (meaning that you don't intend to, say, replace X with Y and then replace Y with Z), then you can rewrite DeepReplace to take a union M of mapping tuples corresponding to [Condition1, Replacement1] | [Condition2, Replacement2] | .... So your old DeepReplace<T, C, R> would be DeepReplace<T, [C, R]>. The definition would look like this:

    type DeepReplace<T, M extends [any, any]> = {
        [P in keyof T]: T[P] extends M[0]
        ? Replacement<M, T[P]>
        : T[P] extends object
        ? DeepReplace<T[P], M>
        : T[P];
    }
    

    where Replacement<M, T> finds the mapping tuple in M where T is assignable to the condition and returns the corresponding replacement, and is defined like this:

    type Replacement<M extends [any, any], T> =
        M extends any ? [T] extends [M[0]] ? M[1] : never : never;
    

    Let's see if it works on some types I'll make up here. Given the following:

    interface DateLike {
        v: Date;
    }
    interface StringLike {
        v: string;
    }
    interface NumberLike {
        v: number;
    }
    
    interface Original {
        a: {
            dat: DateLike;
            str: StringLike;
            num: NumberLike;
            boo: boolean
        },
        b: {
            arr: NumberLike[]
        },
        c: StringLike,
        d: number
    }
    

    Let's replace the ...Like types:

    type Replaced = DeepReplace<Original, 
      [DateLike, Date] | [StringLike, string] | [NumberLike, number]
    >
    
    /* equivalent to
    type Replaced = {
        a: {
            dat: Date;
            str: string;
            num: number;
            boo: boolean;
        };
        b: {
            arr: number[];
        };
        c: string;
        d: number;
    }
    */
    

    So that works.


    Please note that calling the new DeepReplace<T, [C, R]> this probably has the same edge cases the original DeepReplace<T, C, R> has. For example, unions like {a: string | DateLike} won't be mapped. I'll consider any tweaking of these to be outside the scope of the question.


    Okay, hope that helps; good luck!

    Playground link to code