Shane Callanan
Shane Callanan

Reputation: 521

How to copy the structure of one generic type (nested object) to another generic in TypeScript?

This question is very similar to a previous one I made.

I recommend reading my earlier question first: How to copy the structure of one generic type to another generic in TypeScript?


Rather than cloning the structure of a flat object type, I'm looking to clone the structure of a nested object type.

In orther words, I'm looking for a function that, given...

// ...this input type

interface NestedInput {
  name: string;
  arr: [
    string,
    Date,
    {a: boolean}
  ];
  nestedObject: {
    x: number;
    y: number;
  };
}

// ...it produces this output type

type StringMethod = (val: string) => void;
type DateMethod = (val: Date) => void;
type NumMethod = (val: number) => void;
type BoolMethod = (val: boolean) => void;

interface NestedOutput {
  name: StringMethod;
  arr: [
    StringMethod,
    DateMethod,
    {
      a: BoolMethod;
    }
  ];
  nestedObject: {
    x: NumberMethod;
    y: NumberMethod;
  }
}

Once again, it must be completely type-safe, such that I can access output.nestedObject.x or output.arr[2].a using intellisense.

I've been racking my brain the last 2 days trying to figure this out, so any help would be greatly appreciated!


PS: You may have noticed that we run into the problem of defining when we traverse a nested object. A Date object for instance wouldn't be traversed, but some other structure might. To prevent this from being a problem, you can assume that if the object is a vanilla JS object (see function below), then it's ok to traverse.

const getClass: (object: any) => string = Function.prototype.call.bind(Object.prototype.toString);

const isVanillaObject = (obj: any) => {
    return getClass(obj) === "[object Object]";
}

Upvotes: 2

Views: 740

Answers (1)

Sebastian Speitel
Sebastian Speitel

Reputation: 7346

You can use extends to switch on types, a mapped type for the object case and use recursion to allow deep nesting:

interface NestedInput {
  name: string;
  arr: [string, Date, { a: boolean }];
  nestedObject: {
    x: number;
    y: number;
  };
}

type StringMethod = (val: string) => void;
type DateMethod = (val: Date) => void;
type NumMethod = (val: number) => void;
type BoolMethod = (val: boolean) => void;

type Methodify<T> = T extends string
  ? StringMethod
  : T extends Date
  ? DateMethod
  : T extends number
  ? NumMethod
  : T extends boolean
  ? BoolMethod
  : {
      [K in keyof T]: Methodify<T[K]>;
    };

type Output = Methodify<NestedInput>;

//Results in:
type Output = {
    name: StringMethod;
    arr: [StringMethod, DateMethod, {
        a: BoolMethod;
    }];
    nestedObject: {
        x: NumMethod;
        y: NumMethod;
    };
}

Upvotes: 2

Related Questions