Home > Blockchain >  infer typing for nested properties in generic
infer typing for nested properties in generic

Time:10-08

I have two issues. First of all; I want to convert the following structure:

{ isElevated: { sm: true, xl: 'false' }, color: 'green' }

into the following

{ isElevated: { _sm: true, _xl: 'false' }, color: 'green' }

So, I only want to prepend an underscore in keys of a nested object. I've created the following, but I have two issues:

  1. In my current approach I loose typing when (re-)setting nested properties. How can I keep it?
  2. Types of values are widened ('green' becomes string). I would like to keep the literal values without calling the arguments as const.

Link to TS Playground here

type TransformProps<T, K extends keyof T = keyof T> = {
  [Variant in K]: T[Variant] extends object
    ? {
        [NestedKey in keyof T[Variant] as `_${string &
          NestedKey}`]: T[Variant][NestedKey];
      }
    : T[Variant];
};

function transform<T, K extends keyof T>(props: { [Key in K]: T[Key] }) {
  const keys = Object.keys(props) as K[];

  return keys.reduce((acc, k) => {
    if (typeof props[k] === "object") {
      // I'm losing typing here..
      const val = props[k]; // as Record<string, any>;
      const nestedEntries = Object.entries(val);
      const entries = nestedEntries.map(([key, value]) => [`_${key}`, value]);

      return { ...acc, [k]: Object.fromEntries(entries) };
    } else {
      return { ...acc, [k]: props[k] };
    }
  }, {} as TransformProps<T>);
}

const result = transform({
  isElevated: { sm: true, xl: "false" },
  color: "green",
});

const success1 = result.isElevated["_sm"] === true; // Good

// How can I prevent values from widening? (without calling with `as const`)
const fail1 = result.isElevated["_sm"] === false; // Bad
const fail2 = result.color === "red"; // Bad

CodePudding user response:

Your second problem is possible to solve. Your first however, is not really. It's very hard to do these kinds of manipulations while still keeping types accurate inside the implementation. You should probably just do a cast.

We'll use a very special type to remove the need for as const:

type Narrow<T> =
    | (T extends infer U ? U : never)
    | Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
    | ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });

And use it here:

function transform<T, K extends keyof T>(props: { [Key in K]: Narrow<T[Key]> }) {

Now, everything works.

Playground

  • Related