sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
14.09k stars 533 forks source link

Add `Set` type #373

Open ailchenkoDynamo opened 2 years ago

ailchenkoDynamo commented 2 years ago

Add Set type. Looks like Get

Example:

type InitValue = {}

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

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

Upvote & Fund

Fund with Polar

sindresorhus commented 2 years ago

Yes, that would be welcome.

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

skarab42 commented 2 years ago

@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 ;)

ilchenkoArtem commented 2 years ago

@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 commented 2 years ago

My last code for this issue

ehoogeveen-medweb commented 2 years ago

@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:

ehoogeveen-medweb commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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.

sindresorhus commented 1 year ago

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