Home > Net >  TypeScript: typed path to nested property (as tuple)
TypeScript: typed path to nested property (as tuple)

Time:09-30

I'm learning functional programming and trying to implement assoc function (functional setter) as a higher order function with the support of nested properties. I'm okay with the depth being limited by the number of function overloads.

What I'm having now.

The actual implementation:

export type KeyOf<T> = T extends object ? keyof T : never;
export type Setter<S, A> = (whole: S) => (part: A) => S;

export function assoc<T, K1 extends KeyOf<T> = KeyOf<T>>(
  k1: K1
): Setter<T, T[K1]>;

export function assoc<
  T,
  K1 extends KeyOf<T> = KeyOf<T>,
  K2 extends KeyOf<T[K1]> = KeyOf<T[K1]>
>(k1: K1, k2: K2): Setter<T, T[K1][K2]>;

export function assoc<
  T,
  K1 extends KeyOf<T> = KeyOf<T>,
  K2 extends KeyOf<T[K1]> = KeyOf<T[K1]>,
  K3 extends KeyOf<T[K1][K2]> = KeyOf<T[K1][K2]>
>(k1: K1, k2: K2, k3: K3): Setter<T, T[K1][K2][K3]>;

export function assoc<
  T,
  K1 extends KeyOf<T> = KeyOf<T>,
  K2 extends KeyOf<T[K1]> = KeyOf<T[K1]>,
  K3 extends KeyOf<T[K1][K2]> = KeyOf<T[K1][K2]>,
  K4 extends KeyOf<T[K1][K2][K3]> = KeyOf<T[K1][K2][K3]>
>(k1: K1, k2: K2, k3: K3, k4: K4): Setter<T, T[K1][K2][K3][K4]>;

export function assoc(...path: any[]): any {
  return (whole: any) => {
    return (value: any) => {
      return assocDeep(whole, path as any, value);
    };
  };
}

But when I use it with three keys (or more), it seems it can't resolve keys deeper than the second level:

import { assoc } from './assoc';

type Company = {
  readonly id: number;
  readonly name: string;
  readonly address: Address;
};

type Address = {
  readonly country: string;
  readonly region: string;
  readonly street: Street;
  readonly building: string;
};

type Street = {
  readonly name: string;
  readonly kind: string;
};

const setStreetName = assoc<Company>('address', 'street', 'name');
                                                          ~~~~~~

As result the TypeScript compiler says:

Argument of type 'string' is not assignable to parameter of type 'never'. ts(2345)

How can I make it work for more levels of nesting?

CodePudding user response:

First, we'll get a list (as a union) of all the possible paths:

type PathsOf<T> = {
  [K in keyof T]: [K] | (T[K] extends object ? [K, ...PathsOf<T[K]>] : never);
}[keyof T];
export function assoc<T>() {
  return function<P extends PathsOf<T>>(...path: P): MakeSetter<T, P> {
    return null!; // your impl here (might need casting...)
  }
}

Then we'll get the paths of whatever was given to assoc and use that for our path parameter. Note that you have to use currying as partial type inference is not available to us.

Here's MakeSetter:

type MakeSetter<T, P, Original = T> = P extends [infer First extends keyof T, ...infer Rest] ? MakeSetter<T[First], Rest, Original> : Setter<Original, T>;

We're just "iterating" over the path and getting the next value, and then when we're done (no more keys), we return our setter.

Playground

  • Related