Key path manipulations and functional patterns

#Key path manipulations

Imagine you need to create an interface where each key represents deepest nested value of input object and value is a tuple which represents a path to current value. A bit vogue description, is not it? Here you have an example:



type Data = {
    k2: {
        k2A: {
            k2A1: "k2A1_E",
            k2A2: "k2A2_F",
        },
        k2B: {
            k2B1: "k2B1_G",
            k2B2: "k2B2_H",
        },
    }
}

type Iterate<T> = T

// type Result = {
//     k2A1_E: ["k2", "k2A", "k2A1"];
//     k2A2_F: ["k2", "k2A", "k2A2"];
//     k2B1_G: ["k2", "k2B", "k2B1"];
//     k2B2_H: ["k2", "k2B", "k2B2"];
// }
type Result = Iterate<Data>

In order to make it work, we need recursively iterate through input object and accumulate all passed props.


type Data = {
    k2: {
        k2A: {
            k2A1: "k2A1_E",
            k2A2: "k2A2_F",
        },
        k2B: {
            k2B1: "k2B1_G",
            k2B2: "k2B2_H",
        },
    }
}

type Iterate<Obj, Path extends any[] = []> =
    {
        /**
         * Iterate recursively through each key/value pair
         */
        [Prop in keyof Obj]:
        /**
         * If iteration hit the bottom call
         * Iterate recursively but without adding current Prop to
         * Path tuple
         */
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        /**
         * If Obj[Prop] is a nested object, call
         * Iterate recursively with adding Prop to
         * Path tuple
         */
        : Iterate<Obj[Prop], [...Path, Prop]>
    }


type Result = Iterate<Data>

However, we still need to handle a case when we need to exit from the iteration. We need to do this when Obj generic parameter is no more object anymore. Please keep in mind that we already have a branch with Obj[Prop] extends string ? Iterate<Obj[Prop], Path>. In this caseIterate is called with primitive value Obj[Prop]


type Iterate<Obj, Path extends any[] = []> =
    Obj extends string // <----------- added condition
    ? Record<Obj, Path> // <---------- added condition branch
    : {
        /**
         * Iterate recursively through each key/value pair
         */
        [Prop in keyof Obj]:
        /**
         * If iteration hit the bottom call
         * Iterate recursively but without adding current Prop to
         * Path tuple
         */
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        /**
         * If Obj[Prop] is a nested object, call
         * Iterate recursively with adding Prop to
         * Path tuple
         */
        : Iterate<Obj[Prop], [...Path, Prop]>
    }

We ended up with weird type


type Result = {
    k2: {
        k2A: {
            k2A1: Record<"k2A1_E", ["k2", "k2A"]>;
            k2A2: Record<"k2A2_F", ["k2", "k2A"]>;
        };
        k2B: {
            k2B1: Record<"k2B1_G", ["k2", "k2B"]>;
            k2B2: Record<"k2B2_H", [...]>;
        };
    };
}

This is because we forgot to add [keyof Obj] to the end of iteration loop.


type Iterate<Obj, Path extends any[] = []> =
    Obj extends string
    ? Record<Obj, Path>
    : {
        /**
         * Iterate recursively through each key/value pair
         */
        [Prop in keyof Obj]:
        /**
         * If iteration hit the bottom call
         * Iterate recursively but without adding current Prop to
         * Path tuple
         */
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        /**
         * If Obj[Prop] is a nested object, call
         * Iterate recursively with adding Prop to
         * Path tuple
         */
        : Iterate<Obj[Prop], [...Path, Prop]>
    }[keyof Obj] // <-------------- added

Why this change is so important? This trick is also used in type Values<T> = T[keyof T]


type Values<T> = T[keyof T] // <-------------- added

type Iterate<Obj, Path extends any[] = []> =
    Obj extends string
    ? Record<Obj, Path>
    : Values<{ // <-------------- added 
        [Prop in keyof Obj]:
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        : Iterate<Obj[Prop], [...Path, Prop]>
    }>


type Result = Iterate<Data>

It helps us to obtain only values of iterated object. We almost there


type Iterate<Obj, Path extends any[] = []> =
    Obj extends string
    ? Record<Obj, Path>
    : {
        [Prop in keyof Obj]:
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        : Iterate<Obj[Prop], [...Path, Prop]>
    }[keyof Obj] // <----- small revert for readability


// type Result =
//     | Record<"k2A1_E", ["k2", "k2A"]>
//     | Record<"k2A2_F", ["k2", "k2A"]>
//     | Record<"k2B1_G", ["k2", "k2B"]>
//     | Record<"k2B2_H", ["k2", "k2B"]>

type Result = Iterate<Data>

We ended up with a union of all expected key/value pairs. The only thing is left to do is to intersect them (merge)



type Data = {
    k2: {
        k2A: {
            k2A1: "k2A1_E",
            k2A2: "k2A2_F",
        },
        k2B: {
            k2B1: "k2B1_G",
            k2B2: "k2B2_H",
        },
    }
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

type Iterate<Obj, Path extends any[] = []> =
    Obj extends string
    ? Record<Obj, Path>
    : {
        [Prop in keyof Obj]:
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        : Iterate<Obj[Prop], [...Path, Prop]>
    }[keyof Obj]


// type Result =
//     & Record<"k2A1_E", ["k2", "k2A"]>
//     & Record<"k2A2_F", ["k2", "k2A"]>
//     & Record<"k2B1_G", ["k2", "k2B"]>
//     & Record<"k2B2_H", ["k2", "k2B"]>

type Result = UnionToIntersection<Iterate<Data>>

As you might have noticed, it is hard to read output type, this is why you can make it more readable:



type Data = {
    k2: {
        k2A: {
            k2A1: "k2A1_E",
            k2A2: "k2A2_F",
        },
        k2B: {
            k2B1: "k2B1_G",
            k2B2: "k2B2_H",
        },
    }
}

// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
    k: infer I
) => void
    ? I
    : never;

type Iterate<Obj, Path extends any[] = []> =
    Obj extends string
    ? Record<Obj, Path>
    : {
        [Prop in keyof Obj]:
        Obj[Prop] extends string
        ? Iterate<Obj[Prop], Path>
        : Iterate<Obj[Prop], [...Path, Prop]>
    }[keyof Obj]


// type Result =
//     & Record<"k2A1_E", ["k2", "k2A"]>
//     & Record<"k2A2_F", ["k2", "k2A"]>
//     & Record<"k2B1_G", ["k2", "k2B"]>
//     & Record<"k2B2_H", ["k2", "k2B"]>

type Result = UnionToIntersection<Iterate<Data>>

This is exactly what we are looking for. Here you can find related answer.

Btw, small of topic. As you might have noticed, I have used Debug utility type to make it easier to read. If you are interested in debugging more complex union types you can check my question

#Functional programming utils

In this part of the article I'd like to show you several functional patterns I came across working on React projects.

Imagine you have some form with a lot of inputs


import React, { useState } from 'react'
import { compose, } from 'redux'
import { useDispatch } from 'react-redux'


type State = {
    name: string;
    age: number;
}

type ActionPayload = `${string}/${number}`

const action = (payload: ActionPayload) =>
    ({ type: 'PULL', payload })


const toPayload = ({ name, age }: State): ActionPayload => `${name}/${age}`

const Form = () => {
    const dispatch = useDispatch()
    const [config, handleConfig] = useState<State>({
        name: 'John',
        age: 42
    })

    const callAction = () => {
        dispatch(action(toPayload(config)))
    }

    return (
        <form>
        // a lot of inputs
            <button onClick={callAction} />
        </form>
    )
}

I'm interested in callAction. In this case, we can usecompose function from redux package. I'd willing to bet that since you are working with React you have it in your dependencies.


const Form = () => {
    const dispatch = useDispatch()
    const [config, handleConfig] = useState<State>({
        name: 'John',
        age: 42
    })
    // CHANGE IS HERE
    const dispatchAction = compose(dispatch, action, toPayload)

    const handleClick = () =>
        dispatchAction(config)

    return (
        <form>
        // a lot of inputs
            <button onClick={handleClick} />
        </form>
    )
}

Let's slightly modify our example


import React, { useState } from 'react'
import { compose, } from 'redux'
import { useDispatch } from 'react-redux'


type State = {
    name: string;
    // TYPE IS CHANGED
    age: string;
    entitlements: string[]
}

type Action = {
    type: 'PULL',
    payload: State
}

const action = (payload: Action['payload']) =>
    ({ type: 'PULL', payload })


const toPayload = ({ name, age, entitlements }: State) =>
({
    name: name.toLowerCase(),
    age: parseInt(age, 10),
    entitlements: entitlements.filter(elem => elem !== 'admin')
})

const Form = () => {
    const dispatch = useDispatch()
    const [config, handleConfig] = useState<State>({
        name: 'John',
        age: '42',
        entitlements: ['read', 'write', 'admin']
    })
    const dispatchAction = compose(dispatch, action, toPayload)

    const handleClick = () =>
        dispatchAction(config)

    return (
        <form>
        // a lot of inputs
            <button onClick={handleClick} />
        </form>
    )
}

Now we need to transform each property in state in order to make it fit to action payload. Above code is perfectly fine. However, in real life, toPayload function is usually much bigger and contains more business logic.

Usually, I'm trying to move each piece of logic into separate function. Consider this example:


import React, { useState } from 'react'
import { compose, } from 'redux'
import { useDispatch } from 'react-redux'


type State = {
    name: string;
    age: string;
    entitlements: string[]
}

type Action = {
    type: 'PULL',
    payload: State
}

const action = (payload: Action['payload']) =>
    ({ type: 'PULL', payload })


const isNotAdmin = (entitlement: string) => entitlement !== 'admin'

const convertName = ({ name, ...rest }: State) => ({
    ...rest,
    name: name.toLowerCase(),
})

const convertAge = ({ age, ...rest }: State) => ({
    ...rest,
    age: parseInt(age, 10)
})

const convertEntitlements = ({ entitlements, ...rest }: State) => ({
    ...rest,
    entitlements: entitlements.filter(isNotAdmin)
})

const toPayload = compose(convertAge, convertName, convertEntitlements)

const Form = () => {
    const dispatch = useDispatch()
    const [config] = useState<State>({
        name: 'John',
        age: '42',
        entitlements: ['read', 'write', 'admin']
    })
    const dispatchAction = compose(dispatch, action, toPayload)

    const handleClick = () =>
        dispatchAction(config)

    return (
        <form>
        // a lot of inputs
            <button onClick={handleClick} />
        </form>
    )
}

As you might have noticed, each function is now composable. I know, it seems redundant to create 4 extra functions for this case, and you are, probably right. However, in a big project, you may find more patterns, and all these functions might be reusable in other files/components. If according to project style guide it is required to use FP approach above examples might be helpful. Otherwise, it might be not good idea to use this approach only in one component.

I want to thankblog.feedspot.com for mentioning my blog in their list.

February 21, 2022