typescripttypescript-typings

How can I make an interface in TypeScript that only accepts keys with identical value types between two objects?


How can I make an interface in TypeScript that only accepts keys with identical value types between two objects? I'm having trouble creating interface..

interface A {
    aString: string;
    aNumber: number;
}

interface B {
    bString: string;
    bNumber: number;
}

interface C {
    aKey: keyof A;
    bKey: keyof B;
    // but only wants to accept keyof B when only typeof current A[keyof A] === typeof B[keyof B]
}


I'm expecting the result


const c1: C = { // success typeof A.aString === typeof B.bString
    aKey: 'aString',
    bKey: 'bString', 
}

const c2: C = { // fail typeof A.aString !== typeof B.bNumber
    aKey: 'aString',
    bKey: 'bNumber',
}

Is there anyway to do this?.. ChatGPT suggests to filter the keyof B that only contained in values of A but it only makes every keyof B possible.


Solution

  • There's no specific interface type that works this way, since you can't make one property depend on another. You could make a generic interface, but then you'd need to specify or infer the generic type argument all over the place and that could be annoying. Since you have a finite number of properties to worry about, you can represent C as a union type instead of an interface. It would look like

    type C = {
        aKey: "aString";
        bKey: "bString";
    } | {
        aKey: "aNumber";
        bKey: "bNumber";
    }
    

    so that only the two valid pairings of aKey and bKey work:

    const c1: C = {
      aKey: 'aString',
      bKey: 'bString',
    } // okay
    
    const c2: C = {
      aKey: 'aString',
      bKey: 'bNumber',
    } // error
    

    The above definition for C was written out manually, but you can make the compiler compute it by iterating over the keys of (say) A, and finding each property key of B whose property matches. This would need some sort of KeysMatching<T, V> type which gives you the keys of T whose values are compatible with V. There is no built-in operator for this (see microsoft/TypeScript#48992 for the feature request), but you can write your own in various ways; here's one way:

    type KeysMatching<T, V> = 
      keyof { [K in keyof T as T[K] extends V ? K : never]: 0 };
    

    That uses key remapping to filter keys by property type. Then C can be written as

    type C = { 
      [K in keyof A]: { aKey: K, bKey: KeysMatching<B, A[K]> } 
    }[keyof A];
    

    That's a so-called distributive object type where I make a mapped type that computes the desired type for each key of A, and then immediately index into it to get the desired union.

    You can verify that the above evaluates to the same definition for C as the manual one. And if you add/change properties to A and B, the definition of C will change accordingly. There's a wrinkle that if you have multiple keys of A with the same property type you might get a less optimized union type as a result; imagine {aKey: "x", bKey: "z"} | {aKey: "y", bKey: "z"} instead of {aKey: "x" | "y", bKey: "z"}, but it will still be correct.

    Playground link to code