By using the type signature (keyof FidelityCheckRow)[] you specify that each key can be any of the keys of FidelityCheckRow, so also the one with a number value. Without the cast the type will be string[] which is also not what you want, but if you declare keys as a tuple by using as const the type of key will get narrowed down correctly:
interface FidelityCheckRow {
P1: number;
P2: string;
P3: string;
}
const keys = ['P2', 'P3'] as const
function test(a: FidelityCheckRow) {
keys.forEach(key => {
a[key] = a[key]?.toString()?.trim()
})
}
TypeScript playground
About validating the keys:
Since we use keys as a tuple to preserve the exact literal types, we can't use a type signature to validate those keys, as the resulting type would no longer be specific. Incorrect keys will trigger errors on use though (e.g. at a[key]).
There are workarounds to make sure keys only has valid keys, for example by creating a generic type that constrains its parameter to an array of valid keys:
type AssertKeys<K extends ReadonlyArray<keyof FidelityCheckRow>> = never
You can use it by putting a dummy Assertion type near your keys definition and passing it the type of the keys tuple:
const badKeys = ['P2', 'P3', 'P4'] as const
type Assertion = AssertKeys<typeof badKeys>
// ERROR: ... Type '"P4"' is not assignable to type 'keyof FidelityCheckRow'.
Another option is to use a constructor function to create the keys, which is just an identity function that restricts its argument to valid key tuples:
const mkKeys = (ks: ReadonlyArray<keyof FidelityCheckRow>) => ks
If you use mkKeys to create the key tuple, any invalid keys will be flagged:
const mkKeys = <K extends ReadonlyArray<keyof FidelityCheckRow>>(ks: K) => ks
const goodKeys = mkKeys(['P2', 'P3']) // No error
const badKeys = mkKeys(['P2', 'P3', 'P4'])
// ERROR: Type '"P4"' is not assignable to type 'keyof FidelityCheckRow'.
And while we're at it, we might as well parameterize the constructor with the object type and the desired value type, so you can use it for other interfaces than FidelityCheckRow, and other value types than string:
type FilterKeysByValue<T, V> = keyof {
[K in keyof T as T[K] extends V ? K : never]: never
}
const mkKeys = <T, V>(ks: ReadonlyArray<FilterKeysByValue<T, V>>) => ks
The error messages are slightly less descriptive, but with the explicit type parameters around, just a squiggly is probably informative enough to see what's wrong:
const goodKeys = mkKeys<FidelityCheckRow, string>(['P2', 'P3']) // No error
const badKeys1 = mkKeys<FidelityCheckRow, string>(['P1', 'P2', 'P3'])
// ERROR: Type '"P1"' is not assignable to type '"P2" | "P3"'.
const badKeys2 = mkKeys<FidelityCheckRow, number>(['P1', 'P2', 'P3'])
// ERROR: Type '"P2"' is not assignable to type '"P1"'.
// ERROR: Type '"P3"' is not assignable to type '"P1"'.
TypeScript playground