[SOLVED] How to get the shape of an object then modify the type of each leaf property?

Issue

For example, take this interface:

interface SomeObject {
  prop1: number;
  prop2: string;
  prop3: {
    innerProp1: number[];
    innerProp2: string[];
    innerProp3: {
      deeperProp1: string[];
      deeperprop2: boolean;
    },
    innerProp4: {
      [key: string]: any;
    },
    innerProp5: {
      [key: string]: any;
    }
  }
}

I want to create a type that accepts the shape of any object then returns the same object shape but with the provided type for the “leaf” properties and each property of the object can be optional. Something like the one below:

type ModifyShapeType<Shape, NewType> = ???

Such that when used against, for example, the interface above, I’d get type safety for the same object shape but with the provided type:

const myObject: ModifyShapeType<SomeObject, boolean> = {
  prop1: true;
  prop2: true;
  prop3: {
    // innerProp1: true;
    // innerProp2: false;
    innerProp3: {
      deeperProp1: true;
      // deeperprop2: true;
    },
    innerProp4: true,
    // innerProp5: false
  }
};

I’ve come up with the one below but I want to get rid of the original types from the shape and replace it with what I want and if possible, still retain the specificity on property reads and writes.

type ModifyShapeType<S, T> = Partial<Record<keyof S, Partial<S[keyof S] | T>>>;

Here’s a TypeScript Playground.

Caveats currently:

  1. Types still being inferred from the original object type, in fact it’s now all mixed.
  2. All properties now share the same type (specificity on read lost) which also means unsafe writes (specificity on write lost)

Is this even possible?

Solution

Sounds like you need a mapped, recursive type.

To create this, you would iterate through each key, and see if it is an object (branch), or if it’s some other value (leaf). If it’s a branch, then recurse. If it’s a leaf, output the desired value type. The problem of deciding what defines a leaf is a little tricky and application specific (after every value in javascript has properties and can be somewhat object-like).

So you will need a conditional brach detector type, and a recursive mapped type.

// Returns T if the T is a branch. Otherwise it returns `never`.
type IsBranch<T> = 
  // Is T an object?
  T extends { [k: string]: any }

    // T is an object. Is it also an array?
    ? T extends any[]

      // T is an object, but also is an array. This is a leaf.
      ? never

      // T is an object, but is not also an array. This is a branch.
      : T

    // T is not an object. This is a leaf.
    : never

// Recursively process each key.
// If it is a branch, process its keys and return the Partial of that branch.
// If it is a leaf, replace with the value type.
type ModifyShapeType<S, T> = S extends IsBranch<S> ?
  Partial<{ [k in keyof S]: ModifyShapeType<S[k], T> }> :
  T

const a: ModifyShapeType<{ a: number }, boolean> = { a: true }
const b: ModifyShapeType<{ a: number[] }, boolean> = { a: true }
const c: ModifyShapeType<{ a: { b: number } }, boolean> = { a: { b: true } }

Playground

One tricky thing is that an array type looks like an array so we need a special case for that.

Another tricky thing is trying derive the difference between { a: number } and { [k: string]: number }. It seems you want to treat the former as a branch, but the latter as a leaf. And I’m not sure there is a way to do that. There may be away to do a conditional test of T[string] to see if it’s indexable, but I haven’t quite figured that one out.

Answered By – Alex Wayne

Answer Checked By – Robin (BugsFixing Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *