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.
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.