0

I've created a generic which map object, but I have a problem. I need to save optional parameters too

export declare type DeepMap<Values, T> = {
    [K in keyof Values]: 
        Values[K] extends any[] 
            ? Values[K][number] extends object 
                   ? DeepMap<Values[K][number], T>[] | T | T[] : T | T[]
            : Values[K] extends object
            ? DeepMap<Values[K], T>
            : T;
};

I have an original type:

type Obj1 = {
  a: number;
  b: {
    a: number;

  };
  c?: {
    a: number;
  } 
}

I want to get a new type in next way:

type MappedObj1 = {
  a: string;
  b: {
    a: string;

  };
  c?: {
    a: string;
  } 
}

But now I have only this structure

type MappedObj1 = {
  a: string;
  b: {
    a: string;

  };
}

My optional type was lost

Could you help me please how can I map a deep object and save all parameters with optionals ?

Playground

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • If I'm looking at your example code I'd be inclined to write it [this way](https://tsplay.dev/N7bzrw). But there's a lot going on in your `DeepMap` type, so it would be nice to know what the use cases are for those clauses... like, why `| T | T[]`? I'd like some understanding on why that stuff needs to be preserved. – jcalz Apr 03 '21 at 17:23
  • Great! Thanks a lot. I thing, using ```| T | T[]``` was unnecessary, I forgot to remove it – Vladimir Fomin Apr 03 '21 at 17:37

1 Answers1

2

From the example code, I'd be inclined to use the following typing:

type DeepMap<T, V> =
    T extends undefined ? undefined :
    T extends object ? { [K in keyof T]: DeepMap<T[K], V> } :
    V;

This means that DeepMap<T, V> will turn any non-undefined primitive into V. For undefined, it stays undefined. And for any object, including arrays, it does a recursive mapping of DeepMap of its properties. Since the mapped type is homomorphic, it preserves the optionality of its properties. And since the conditional type is distributive, it will preserve unions. This produces the following type for MappedObj1:

type MappedObj1 = DeepMap<Obj1, string>;
/* type MappedObj1 = {
    a: string;
    b: {
        a: string;
    };
    c?: {
        a: string;
    } | undefined;
} */

A note about why I special-cased undefined: Observe that optional properties include undefined in their value types. So {c?: {a: number}} is the same as {c?: {a: number} | undefined}. If you want that to map to {c?: {a: string} | undefined} and not {c?: {a: string} | string}, and if you want both homomorphic and distributive mappings, then you need undefined to map to undefined and not string.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360