Search code examples
typescriptgenericsfunctional-programmingconstraintsmapped-types

Typescript: Place constraint on specific lookup type of generic object type


Alright so here's the problem. I have the following type and function definitions:

export type compareFunction<T> = (t1: T, t2: T) => boolean

function createCompareFunctionCreator<P>(customCompare: compareFunction<P>) {
    return <
        T,
        K extends keyof T
    >(propName: K) => {
        return (t1: T, t2: T) => customCompare(t1[propName], t2[propName]) as compareFunction<P>
    }
}

My question is, how would place a particular constraint such that T[propName] is of type P?

I've tried the following:

function createCompareFunctionCreator<P>(customCompare: compareFunction<P>) {
    return <
        T extends { [keys in keyof T]: P },
        K extends keyof T
    >(propName: K) => {
        return (t1: T, t2: T) => customCompare(t1[propName], t2[propName]) as compareFunction<P>
    }
}

But this forces ALL properties in T to map to type P.


Solution

  • Force T to have all properties of T

    The problem is lack of relation between P and T. We can solve that but setting the relation. Consider following code:

    export type compareFunction<T> = (t1: T, t2: T) => boolean
    
    function createCompareFunctionCreator<P>(customCompare: compareFunction<P>) {
        return <
            T extends Record<K, P>, // pay attention here
            K extends keyof T
        >(propName: K) => {
            return (t1: T, t2: T) => customCompare(t1[propName], t2[propName])
        }
    }
    

    T extends Record<K, P> is saying that our type T is an object which all properties are type of P. Thanks to that we can do t1[propName] and we know that its type of P.

    Force T to have properties of P with other properties

    We can achieve that by some additional typing consider:

    // utility type which gives us only keys which values in T1 are T2
    type OnlyKeysOfT<T1, T2> = {
        [K in keyof T1]: T1[K] extends T2 ? K : never
    }[keyof T1]
    
    function createCompareFunctionCreator<P>(customCompare: compareFunction<P>) {
        return <
            T extends Record<K, P>,
            K extends OnlyKeysOfT<T, P> = OnlyKeysOfT<T, P>,
        >(propName: K) => {
            return (t1: T, t2: T) => customCompare(t1[propName], t2[propName])
        }
    }
    // below only a is correct
    createCompareFunctionCreator<string>((a,b) => true)<{a: string, b: number}>('a')