When it comes to interfaces, they are purely a compile-time concept and do not exist at runtime. This means that using a type in an expression poses a challenge since the type is not available during code execution.
To work around this limitation, we can create an object that includes all the keys of the interface - a collection that is guaranteed by the compiler to consist solely of those keys.
By leveraging this object in a custom type-guard, we can assist the compiler in narrowing down the type of the key being used.
An approach to achieve this would resemble the following:
interface A {
a1: string;
a2: number;
a3?: boolean;
}
interface B {
b1: number;
b2: boolean;
b3: string;
}
// Factory function for key type-guards
function interfaceKeys<T>(keys: Record<keyof T, 0>) {
return function (o: PropertyKey): o is keyof T {
return o in keys;
}
}
// The objects here are compiler enforced to have all the keys and nothing but the keys of each interface
const isAkey = interfaceKeys<A>({ a1: 0, a2: 0, a3: 0 })
const isBkey = interfaceKeys<B>({ b1: 0, b2: 0, b3: 0 })
function foo<K1 extends keyof A, K2 extends keyof B>(input: K1 | K2) {
if (isAkey(input)) { // custom type guard usage
console.log('got A type');
input // is K1
} else {
console.log('got B type');
input // is K2
}
}
foo('a1');
foo('b2');