Search code examples
typescripttypesswap

How to define a Typescript function that swaps values of two object properties by name, with check of type compatibility?


I'm trying to define a function that swaps values of two properties on an object given their names, but I would like compiler to check types compatibility (or at least check both properties to have same type):

function swap<T, TKey1 extends keyof T, TKey2 extends keyof T>(obj: T, key1: TKey1, key2: TKey2): void{
    let temp = obj[key1];
    obj[key1] = obj[key2]; 
    obj[key2] = temp;
}


let obj = {
    a: 1,
    b: 2,
    c: ""
}

swap(obj, "a", "b");    // good, both are numbers
swap(obj, "a", "c");    // should not compile, swapping number with string

TS playground

I got some result with the following, but it requires obj to be passed twice.

function swap<T,
    TKey1 extends keyof T,
    TKey2 extends keyof T,
    TIn extends { [p in TKey1|TKey2]: T[TKey1] } >(_:T, obj: TIn, key1: TKey1, key2: TKey2): void{
    let temp = <any>obj[key1];
    obj[key1] = <any>obj[key2]; 
    obj[key2] = temp;
}


let obj = {
    a: 1,
    b: 2,
    c: ""
}

swap(obj, obj, "a", "b");    // good, both are numbers
swap(obj, obj, "a", "c");    // error, as expected

TS playground

Alternatively, I can achieve desired result with conditional types if I return a function, but it is too easy to forget second call.

function swap<T,
    TKey1 extends keyof T,
    TKey2 extends keyof T>(obj: T, key1: TKey1, key2: TKey2):
                                            T[TKey1] extends T[TKey2] ? T[TKey2] extends T[TKey1] 
                                                ? () => void
                                                : never : never {

    return <any>(() => {
        let temp = <any>obj[key1];
        obj[key1] = <any>obj[key2];
        obj[key2] = temp;
    });
}


let obj = {
    a: 1,
    b: 2,
    c: ""
}

swap(obj, "a", "b")();    // good, both are numbers
swap(obj, "a", "c")();    // error, as expected

TS playground

Is it possible to simplify above examples? Can I maybe supply some type instead of never that will indicate error to type system?

P.S. I know about [obj.a, obj.b] = [obj.b, obj.a]; , but would like to avoid it.


Solution

  • Ok, they key turned out to be filtering the second key with advanced types.

    Source code available: https://github.com/IKoshelev/ts-typing-util/blob/master/src/Swap.ts

    NPM installation npm i ts-typing-util

    export type SwappableKeys<T, TKey1 extends keyof T> = Exclude<{
        [key in keyof T]:
        /**/ T[key] extends T[TKey1]
        /**/ ? T[TKey1] extends T[key]
        /*      */ ? key
        /*      */ : never
        /**/ : never;
    
    }[keyof T], TKey1>;
    
    /**
     * Swap prop values with a check that values have compatible type
     * @example
     * const t = {
     *   a: 1,
     *   b: 2,
     *   c: '',
     *   c1: '',
     *   d: { a: 5 },
     *   e: { a: 6 },
     *   f: { b: 7 },
     *   g: { a: '' }
     * }
     *
     * swap(t, 'a', 'b');
     * swap(t, 'a', 'c'); //error
     * swap(t, 'b', 'c'); //error
     * swap(t, 'a', 'a'); //error
     * swap(t, 'c', 'c1');
     * swap(t, 'd','e');
     * swap(t, 'd','f'); //error
     * swap(t, 'd','g'); //error
     **/
    export function swap<T, TKey1 extends keyof T>(inst: T, key1: TKey1, key2: SwappableKeys<T, TKey1>): void {
    
        const buff = inst[key1] as any;
        inst[key1] = inst[key2] as any;
        inst[key2] = buff;
    
    }
    
    

    Playground Example