type-fest icon indicating copy to clipboard operation
type-fest copied to clipboard

Add `Set` type

Open ailchenkoDynamo opened this issue 2 years ago • 9 comments

Add Set type. Looks like Get

Example:

type InitValue = {}

type NewResult = Set<InitValue , 'level1.level2.level3',  number>

result:  {
 level1: {
   level2: {
     level3: number
   }
 }
}

Upvote & Fund

  • We're using Polar.sh so you can upvote and help fund this issue.
  • The funding will be given to active contributors.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar

ailchenkoDynamo avatar Mar 08 '22 11:03 ailchenkoDynamo

Yes, that would be welcome.

I personally could use it here: https://github.com/sindresorhus/dot-prop/blob/84c32335c873d24950d07be648ec4f80f0a8664e/index.d.ts#L66-L70

sindresorhus avatar Mar 11 '22 20:03 sindresorhus

@ailchenkoDynamo Set is standard since ES2015, we need to find another name and maybe rename/alias Get?

GetProp / SetProp ?

@sindresorhus I am working on it ;)

skarab42 avatar Apr 05 '22 15:04 skarab42

@skarab42 @sindresorhus I started work on it. I created type for get object by string path. I wil have to do creatе Deep Merge for two object Base object and object by path. I I will try to finish it when I have time.

My current code

Maybe I can solve this problem easier without DeepMerge. I will must try

ilchenkoArtem avatar May 20 '22 21:05 ilchenkoArtem

My last code for this issue

ilchenkoArtem avatar May 22 '22 19:05 ilchenkoArtem

@ilchenkoArtem This is great, thank you! One issue I ran into: The path doesn't accept the square bracket syntax for array indices. Changing a property nested in an array does work, but the dot syntax is required:

  • type NewType = SetProp<OldType, 'prop[0].nested[1].in[2].array', NewPropType>; doesn't work (property remains unchanged),
  • type NewType = SetProp<OldType, 'prop.0.nested.1.in.2.array', NewPropType>; does work, though it looks a little awkward.

ehoogeveen-medweb avatar Jun 14 '22 11:06 ehoogeveen-medweb

Another thing that would be useful would be to set/unset readonly on specific nested properties.

In my case the base type has readonly because ts gets confused if I don't create the object with as const (it ends up thinking that string constant properties can be undefined). But I do need the property of the modified type to be writable so I can update the corresponding object.

As an aside, I actually need to update several properties so something like SetProps taking an array (union?) of paths would be ideal. But I'm not sure what that would look if each path also needs a specific type (in my case they all have the same type).

ehoogeveen-medweb avatar Jun 14 '22 13:06 ehoogeveen-medweb

I think arrays need to be handled specially after all, otherwise 1. the resulting type becomes an object with numeric keys and 2. all the standard properties of Array are pulled in as well. I managed to make a type to replace an array element at a given index (recursion based on this answer):

type ArrayReplace<A extends Array<any>, i extends string | number, value, D extends Array<number | string> = []> =
  A extends [infer first, ...infer rest]
  ? i extends D['length'] | `${D['length']}`
    ? [value, ...rest]
    : [first, ...ArrayReplace<[...rest], i, value, [i, ...D]>]
  : [];
type ArrayReplaceReadonly<A extends ReadonlyArray<any>, i extends string | number, value, D extends Array<number | string> = []> =
  A extends readonly [infer first, ...infer rest]
  ? i extends D['length'] | `${D['length']}`
    ? readonly [value, ...rest]
    : readonly [first, ...ArrayReplaceReadonly<[...rest], i, value, [i, ...D]>]
  : readonly [];

type Test = ArrayReplace<[1, 2, 3], '1', 'foo'>;
type ReadonlyTest = ArrayReplaceReadonly<readonly [1, 2, 3], 1, 'foo'>;

Check the Playground

Due to the instantiation depth limit 48 elements seems to be the maximum array length, but presumably it would be less if called from an already nested SetProp instantiation. It might be possible to extend the limit using the technique from this page.

I haven't integrated this with the WIP SetProp code yet but I'll see if I can do that soon.

ehoogeveen-medweb avatar Jun 15 '22 12:06 ehoogeveen-medweb

I integrated the above code with the WIP SetProp implementation and made a few more tweaks! Here it is:

// Replaces a single element of an array literal at the given position with the given value.
type ArrayReplace<
  A extends Array<unknown>, i extends number, value, D extends Array<unknown> = []
> = (
  A extends [infer first, ...infer rest]
  ? i extends D['length']
    ? [value, ...rest]
    : [first, ...ArrayReplace<[...rest], i, value, [i, ...D]>]
  : []
);

// Replaces a single element of a readonly array literal at the given position with the given value.
type ReadonlyArrayReplace<
  A extends ReadonlyArray<unknown>, i extends number, value, D extends Array<unknown> = []
> = (
  A extends readonly [infer first, ...infer rest]
  ? i extends D['length']
    ? readonly [value, ...rest]
    : readonly [first, ...ReadonlyArrayReplace<[...rest], i, value, [i, ...D]>]
  : readonly []
);

// Gets the key part of a path, with support for array index notation.
type PathGetKey<path extends string> = (
  path extends `${infer keyInd}.${infer rest}`
  ? keyInd extends `${infer key}[${infer index}]`
    ? key
    : keyInd
  : path extends `${infer key}[${infer index}]`
    ? key
    : path
);

// Gets the rest of the path, with support for array index notation.
type PathGetRest<path extends string> = (
  path extends `${infer keyInd}.${infer rest}`
  ? keyInd extends `${infer key}[${infer index}]`
    ? `${index}.${rest}` // Return dot notation to simplify PathGetKey.
    : rest
  : path extends `${infer key}[${infer index}]`
    ? index
    : ''
);

// TypeScript 4.5 - 4.7.
type Mapped<N extends number, Result extends Array<unknown> = []> =
  Result['length'] extends N ? Result : Mapped<N, [...Result, Result['length']]>;
type NumberRange = Mapped<999>[number];
type ConvertToNumber<T extends string, Range extends number = NumberRange> =
  Range extends unknown ? (`${Range}` extends T ? Range : never) : never;

// TypeScript 4.8+.
/*
type ConvertToNumber<T extends string> =
  T extends `${infer R extends number}` ? R : never;
*/

// Replaces the value at the given index in the array with an updated type.
type SetPropArray<
  A extends Array<unknown>, value, keyName extends string,
  restPath extends string, index extends number = ConvertToNumber<keyName>
> = (
  restPath extends ''
  ? index extends A['length']
    ? [...A, value] // Allow appending elements to the array.
    : ArrayReplace<A, index, value>
  : A[index] extends object
    ? ArrayReplace<A, index, SetProp<A[index], restPath, value>>
    : never // Require passing the parent type with a shorter path.
);

// Replaces the value at the given index in the readonly array with an updated type.
type SetPropReadonlyArray<
  A extends ReadonlyArray<unknown>, value, keyName extends string,
  restPath extends string, index extends number = ConvertToNumber<keyName>
> = (
  restPath extends ''
  ? index extends A['length']
    ? readonly [...A, value] // Allow appending elements to the array.
    : ReadonlyArrayReplace<A, index, value>
  : A[index] extends object
    ? ReadonlyArrayReplace<A, index, SetProp<A[index], restPath, value>>
    : never // Require passing the parent type with a shorter path.
);

// Replaces the value with the given key in the object with an updated type.
type SetPropObject<
  O extends object, value, keyName extends string, restPath extends string
> = {
  [key in (keyName extends keyof O ? keyof O : keyof O | keyName)]:
    key extends keyof O
    ? keyName extends key
      ? restPath extends ''
        ? value // Place the value.
        : O[key] extends object
          ? SetProp<O[key], restPath, value>
          : never // Require passing the parent type with a shorter path.
      : O[key] // Return the original value.
    : restPath extends ''
      ? value // Place the value.
      : never // Require passing the parent type with a shorter path.
} extends infer R ? R : never; // Improve on hover type information.

export type SetProp<
  Input extends object, path extends string, value,
  keyName extends string = PathGetKey<path>, restPath extends string = PathGetRest<path>
> = (
  Input extends Array<unknown>
  ? SetPropArray<Input, value, keyName, restPath>
  : Input extends ReadonlyArray<unknown>
    ? SetPropReadonlyArray<Input, value, keyName, restPath>
    : SetPropObject<Input, value, keyName, restPath>
);

Check the Playground

Unfortunately it requires TypeScript 4.8 (the current Nightly) for keyName extends `${infer index extends number}` in SetPropArray, see e.g. this answer. Since large array indices aren't really supported anyway it could be adjusted to work with older versions of TypeScript, but for me this is good enough.

Edit: I modified it to work on TypeScript 4.5 through 4.7 as well based on that SO answer. Edit: Fixed a bug in SetPropArray/SetPropReadonlyArray where trying to append an element would prepend instead.

ehoogeveen-medweb avatar Jun 15 '22 16:06 ehoogeveen-medweb

If anyone wants to work on this, see the initial attempt and feedback in: https://github.com/sindresorhus/type-fest/pull/409

sindresorhus avatar Jun 14 '23 12:06 sindresorhus