florian norbert bepunkt
florian norbert bepunkt

Reputation: 2539

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>

Upvotes: 4

Views: 2459

Answers (1)

jcalz
jcalz

Reputation: 329418

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

Upvotes: 7

Related Questions