2

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?

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

4 Comments

thanks! it works well for many levels, hooray!
is it possible to avoid separation into two functions? It requires an extra call without arguments...
@AlexChandler I don't think so, or else we can't give the return type an accurate type. See the linked question "partial type inference".
Thanks, you saved me a lot of time! I'll try to dig deeper into the solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.