I'm having an issue wherein TypeScript is not exhaustively checking types when the keyof
operator is used on types that extend indexable types.
So, the normal use-case of the keyof operator would be something like a Pick<T1, T2>
type:
interface Person {
name: string;
age: number;
alive: boolean;
}
function getPersonInfo<K extends keyof Person>(person: Person, prop: K): Person[K] {
return person[prop];
}
getPersonInfo({ name: "John", age: 31, alive: true }, "age"); // compiles and returns 31
getPersonInfo({ name: "John", age: 31, alive: true }, "ssn"); // does NOT compile because "ssn" does not match type of "name" | "age" | "alive"
But when an indexable type is involved, we no longer get this safety of keyof
(i.e. a compile-time error telling us that the provided argument is not a key of the type in question). Such as the following:
interface Collection {
[item: string]: any;
}
interface MyCollection extends Collection {
thing: string,
shmoolean: boolean,
lumber: number
}
function getInfo<C extends Collection, K extends keyof C = keyof C>(collection: C, key: K): C[K] {
return collection[key];
}
let c: MyCollection = undefined as unknown as MyCollection;
getInfo<MyCollection>(c, "something"); // compiles even though "something" is not a property of MyCollection
Now, I think I understand why this is occurring, and I believe it is due to the fact that on interfaces that extend indexable types, the 'indexable signature' (if you will) is still present on the type, meaning that the type returned by keyof
effectively gets widened to string
.
So my question is there any way to prevent this from happening? I need to be able to tag classes with their parent indexable type in order to use them in my type system, so I can't just eliminate the indexable type, but I would also like to be able to exhaustively check the type of provided arguments to keyof
types and to be given compile-time errors when a non-key is used.
EDIT1: I found a somewhat related post about removing the index signature from a type by using a wonky type that strips said index signature from a type:
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
So in effect, the new function would be:
function getInfo<C extends Collection, K extends KnownKeys<C> = KnownKeys<C>>(collection: C, key: K): C[K] {
return collection[key];
}
I think this is the direction in which I need to go, but there now I have two problems related to this:
- The example I just gave above does not compile, failing with an error telling me that the type
C
cannot be indexed by the typeKnownKeys<C>
, or what it has narrowed the type down to which isunknown
... I solved this by using a conditional type like this for the return type:
K extends keyof C ? C[K] : never
- This still doesn't seem to be allowing the TypeScript compiler to exhaustively check the type of
keyof
...