Skip to content

Instantly share code, notes, and snippets.

@imekachi
Created August 9, 2023 02:43
Show Gist options
  • Save imekachi/dad5ca6ae13abd28e3d1efd3faa3ee36 to your computer and use it in GitHub Desktop.
Save imekachi/dad5ca6ae13abd28e3d1efd3faa3ee36 to your computer and use it in GitHub Desktop.
Type-safe lodash set
import _set from 'lodash/set'
export const set = <
Obj extends UnknownObject,
KeyPath extends Path<Obj>,
Value extends PathValue<Obj, KeyPath>
>(
obj: Obj,
path: KeyPath,
value: Value
) => _set(obj, path, value)
/**
* Helper function to break apart T1 and check if any are equal to T2
*
* See {@link IsEqual}
*/
export type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never
/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link Path}
*/
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? `${K}`
: true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`
/**
* Helper type for recursively constructing paths through a type.
* This obscures the internal type param TraversedTypes from exported contract.
*
* See {@link Path}
*/
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>
}[TupleKeys<T>]
: PathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>
}[keyof T]
/**
* Type which eagerly collects all paths through a type
* @typeParam T - type which should be introspected
* @example
* ```
* Path<{foo: {bar: string}}> = 'foo' | 'foo.bar'
* ```
*/
export type Path<T> = T extends any ? PathInternal<T> : never
/**
* Helper type for recursively constructing paths through a type.
* This actually constructs the strings and recurses into nested
* object types.
*
* See {@link ArrayPath}
*/
type ArrayPathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| BrowserNativeObject
? IsAny<V> extends true
? string
: never
: V extends ReadonlyArray<infer U>
? U extends Primitive | BrowserNativeObject
? IsAny<V> extends true
? string
: never
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}` | `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
: true extends AnyIsEqual<TraversedTypes, V>
? never
: `${K}.${ArrayPathInternal<V, TraversedTypes | V>}`
/**
* Helper type for recursively constructing paths through a type.
* This obsucres the internal type param TraversedTypes from exported contract.
*
* See {@link ArrayPath}
*/
type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
}[TupleKeys<T>]
: ArrayPathImpl<ArrayKey, V, TraversedTypes>
: {
[K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes>
}[keyof T]
/**
* Type which eagerly collects all paths through a type which point to an array
* type.
* @typeParam T - type which should be introspected.
* @example
* ```
* Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz'
* ```
*/
export type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never
/**
* Type to evaluate the type which the given path points to.
* @typeParam T - deeply nested type which is indexed by the path
* @typeParam P - path into the deeply nested type
* @example
* ```
* PathValue<{foo: {bar: string}}, 'foo.bar'> = string
* PathValue<[number, string], '1'> = string
* ```
*/
export type PathValue<T, P extends Path<T> | ArrayPath<T>> = T extends any
? P extends `${infer K}.${infer R}`
? K extends keyof T
? R extends Path<T[K]>
? PathValue<T[K], R>
: never
: K extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? PathValue<V, R & Path<V>>
: never
: never
: P extends keyof T
? T[P]
: P extends `${ArrayKey}`
? T extends ReadonlyArray<infer V>
? V
: never
: never
: never
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint
export type BrowserNativeObject = Date | FileList | File
/**
* Type which can be used to index an array or tuple type.
*/
export type ArrayKey = number
export type UnknownObject = Record<string, unknown>
export type AnyObject = Record<string, any>
/**
* Checks whether the type is any
* See {@link https://stackoverflow.com/a/49928360/3406963}
* @typeParam T - type which may be any
* ```
* IsAny<any> = true
* IsAny<string> = false
* ```
*/
export type IsAny<T> = 0 extends 1 & T ? true : false
/**
* Checks whether T1 can be exactly (mutually) assigned to T2
* @typeParam T1 - type to check
* @typeParam T2 - type to check against
* ```
* IsEqual<string, string> = true
* IsEqual<'foo', 'foo'> = true
* IsEqual<string, number> = false
* IsEqual<string, number> = false
* IsEqual<string, 'foo'> = false
* IsEqual<'foo', string> = false
* IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean
* ```
*/
export type IsEqual<T1, T2> = T1 extends T2
? (<G>() => G extends T1 ? 1 : 2) extends <G>() => G extends T2 ? 1 : 2
? true
: false
: false
/**
* Type to query whether an array type T is a tuple type.
* @typeParam T - type which may be an array or tuple
* @example
* ```
* IsTuple<[number]> = true
* IsTuple<number[]> = false
* ```
*/
export type IsTuple<T extends ReadonlyArray<any>> = number extends T['length']
? false
: true
/**
* Type which given a tuple type returns its own keys, i.e. only its indices.
* @typeParam T - tuple type
* @example
* ```
* TupleKeys<[number, string]> = '0' | '1'
* ```
*/
export type TupleKeys<T extends ReadonlyArray<any>> = Exclude<
keyof T,
keyof any[]
>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment