/* eslint-disable @typescript-eslint/naming-convention */ /** * Returns number in case `T` is an `Array`, * else if `T extends Record`, it returns the keys of it, * else it returns never. */ export type ToKeys = T extends Array ? number : T extends Record ? { [K in keyof T]: K; }[keyof T] : never; /** * Return type of the pick function. */ export type PickReturn> = T extends Array ? A | undefined : T extends Record ? K extends keyof T ? T[K] | undefined : never : undefined; /** * Takes a value `T` and a key `K`. * If value is an `Array` (`K` is enforced as number in this case), it returns `value[key]` as `A | undefined`. * If value is a `Record` (`K` is enforced as `keyof T` in this case), it returns `value[key]` as `T[K] | undefined`. * Else it returns undefined. */ export const pick = >(value: T | undefined, key: K): PickReturn => { if (Array.isArray(value)) { return value[key]; } if (value !== null && typeof value === 'object') { return (>value)[key] as PickReturn; } return undefined as PickReturn; }; const pickAOrB = (vget: (v: T) => P | undefined, vget2: (v: T2) => P2 | undefined) => (v: T | T2): P | P2 | undefined => { const p = vget(v as T); return p === undefined ? vget2(v as T2) : p; }; /** * An `OptionalLens` grants type-safe access on potential `P`, hence it can be used to * get `P | undefined` from an arbitrary `T`, where `T` might be a union of arbitrary types. * * From an `OptionalLens`, you can also retrieve a new `OptionalLens`, where K is a potential key of P. * Use `getLens()` to get an initial `OptionalLens`. * * You can see `OptionalLens` as "universal optional chaining". While normal optional chaining only works on a `T | null | undefined`, * the `OptionalLens` allows the type-safe access on arbitrary unions. * * E.g. given the following type and values: * ```ts * type TestChild = boolean | { x: number | Array }; * type Test = * | number * | { * a: string | Array; * }; * * const t1: Test = 42; * const t2: Test = { a: 'Test3' }; * const t3: Test = { a: [true, { x: 7 }, { x: [1, 2, 3] }] }; * const t4: TestChild = { x: 7 }; * ``` * * you could get the following results with optional-lenses: * ```ts * const lensA = getLens().k('a'); * const a1 = lensA.get(t1); // => undefined (inferred as undefined | string | Array) * const a2 = lensA.get(t2); // => 'Test3' (inferred as undefined | string | Array) * const a3 = lensA.get(t3); // => [true, { x: 7 }, { x: [1, 2, 3] }] (inferred as undefined | string | Array) * * const lensA2X1 = lensA.k(2).k('x').k(1); * const n1 = lensA2X1.get(t1); // => undefined (inferred as number | undefined) * const n2 = lensA2X1.get(t2); // => undefined (inferred as number | undefined) * const n3 = lensA2X1.get(t3); // => 2 (inferred as number | undefined) * * const lensB = getLens().compose(getLens()); * const t3a = lensB.k('a').get(t3); // [true, { x: 7 }, { x: [1, 2, 3] }] (inferred as undefined | string | Array) * const t4x = lensB.k('x').get(t4); // 7 (inferred as undefined | number | Array) * ``` */ export type OptionalLens = { get: (value: T) => P | undefined; k: >(key: K) => OptionalLens>; compose: (lens: OptionalLens) => OptionalLens; }; const optionalLensKind = '$RXS_OptionalLens$'; type KindedOptionalLens = OptionalLens & { kind: typeof optionalLensKind; }; /** * Typeguard to check, if the given value is an {@link OptionalLens} */ export const isOptionalLens = ( value: OptionalLens | any, ): value is OptionalLens => value?.kind === optionalLensKind; const _toLens = (get: (value: T) => P | undefined): KindedOptionalLens => { return { kind: optionalLensKind, get, k: >(key: K): KindedOptionalLens> => _toLens>((v: T): PickReturn => pick(get(v), key)), compose: (lens: OptionalLens): OptionalLens => _toLens(pickAOrB(get, lens.get)), }; }; /** * Get an `OptionalLens` for type-safe access on arbitrarily nested properties of type `T`, * where `T` might be a union of arbitrary types. * * Given the following type and values: * ```ts * type TestChild = boolean | { x: number | Array }; * type Test = * | number * | { * a: string | Array; * }; * * const t1: Test = 42; * const t2: Test = { a: 'Test3' }; * const t3: Test = { a: [true, { x: 7 }, { x: [1, 2, 3] }] }; * const t4: TestChild = { x: 7 }; * ``` * * you could get the following results with optional-lenses: * ```ts * const lensA = getLens().k('a'); * const a1 = lensA.get(t1); // => undefined (inferred as undefined | string | Array) * const a2 = lensA.get(t2); // => 'Test3' (inferred as undefined | string | Array) * const a3 = lensA.get(t3); // => [true, { x: 7 }, { x: [1, 2, 3] }] (inferred as undefined | string | Array) * * const lensA2X1 = lensA.k(2).k('x').k(1); * const n1 = lensA2X1.get(t1); // => undefined (inferred as number | undefined) * const n2 = lensA2X1.get(t2); // => undefined (inferred as number | undefined) * const n3 = lensA2X1.get(t3); // => 2 (inferred as number | undefined) * * const lensB = getLens().compose(getLens()); * const t3a = lensB.k('a').get(t3); // [true, { x: 7 }, { x: [1, 2, 3] }] (inferred as undefined | string | Array) * const t4x = lensB.k('x').get(t4); // 7 (inferred as undefined | number | Array) * ``` */ export const getLens = (): OptionalLens => _toLens((value: T) => value); /** * Get a concrete `OptionalLens` from an `OptionalLens` (or never, if L is no `OptionalLens`) */ export type ToLensType = [L] extends [OptionalLens] ? OptionalLens : never; /** * Get a concrete `T` from an `OptionalLens` (or never, if L is no `OptionalLens`) */ export type ToLensInputType = [L] extends [OptionalLens] ? T : never; /** * Get a concrete `T` from an `OptionalLens` (or never, if L is no `OptionalLens`) */ export type ToLensOutputType = [L] extends [OptionalLens] ? T : never; /** * Get `T`, if `VL` is an `OptionalLens`, else get `OptionalLens` */ export type ValueOrLens = [VL] extends [OptionalLens] ? ToLensInputType : OptionalLens; /** * Get return type of `OptionalLens::get(T)`, if `X` is an `OptionalLens`, * else get the return type of `OptionalLens::get(T)`, if `Y` is an `OptionalLens`, * else never. */ export type FromLensReturn = [X] extends [OptionalLens] ? ToLensOutputType | undefined : [Y] extends [OptionalLens] ? ToLensOutputType | undefined : never; /** * Utility function to apply an `OptionalLens` to a `T` */ export const fromValueAndLens = (value: T) => >(lens: L): ToLensOutputType | undefined => lens.get(value); /** * Utility function to apply a `T` to an `OptionalLens` */ export const fromLensAndValue = >(lens: L) => >(value: T): ToLensOutputType | undefined => lens.get(value); /** * If the first argument is an `OptionalLens`, this function behaves like `fromLensAndValue`, * else it behaves like `fromValueAndLens` */ export const fromLens = (valueOrLens1: X) => >(valueOrLens2: Y): FromLensReturn => isOptionalLens(valueOrLens1) ? valueOrLens1.get(valueOrLens2) : (>valueOrLens2).get(valueOrLens1); /** * Return type of the toGetter function. * Like {@link OptionalLens}, a `Getter` can be used for optional chaining on arbitrary union types. * In contrast to {@link OptionalLens}, the initial `Getter` must be obtained from a value of type `T`. * You can thus use it as kind of ad-hoc lens for a one-time access. In most cases however, * using an {@link OptionalLens} is the better choice. * * See `toGetter` documentation for example usage. */ export type Getter = { get: () => T; k: >(key: K) => Getter>; }; /** * Helper type to infer the concrete value type `T` wrapped by a `Getter` */ export type ToGetterValue = T extends Getter ? V : never; const _toGetter = (g: () => T): Getter => ({ get: () => g(), k: >(key: K): Getter> => _toGetter(() => pick(g(), key)), }); /** * Wraps the given value of type T in a `Getter`. * A `Getter` can be used like a one-time ad-hoc version of an {@link OptionalLens}. * In most cases however, using an {@link OptionalLens} is the better choice. * * Given the following type and values: * ```ts * type Test = * | number * | { * x: Array; * a: { * b: number; * }; * | { * a: { * b: string; * } * } * }; * * let t1: Test = 42; * let t2: Test = { x: [1, 2, 3] }; * let t3: Test = { a: { b: 'Test' } }; * ``` * * you could get the following results with toGetter: * ```ts * const b1 = toGetter(t1).k('a').k('b').get(); // => undefined (inferred as number | string | undefined) * const b2 = toGetter(t2).k('a').k('b').get(); // => undefined (inferred as number | string | undefined) * const b3 = toGetter(t3).k('a').k('b').get(); // => 'Test' (inferred as number | string | undefined) * const x1 = toGetter(t1).k('x').k(1).get(); // => undefined (inferred as number | undefined) * const x2 = toGetter(t2).k('x').k(1).get(); // => 2 (inferred as number | undefined) * const x3 = toGetter(t3).k('x').k(1).get(); // => undefined (inferred as number | undefined) * ``` */ export const toGetter = (value: T): Getter => _toGetter(() => value);