When using the type signature (keyof FidelityCheckRow)[]
, you are indicating that each key
can be any of the keys found in FidelityCheckRow
, including those with number values. By not casting, the default type would be string[]
, which is not desired. However, if you declare keys
as a tuple using as const
, then the type of key
will be 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
Ensuring valid keys:
Due to using keys
as a tuple to uphold literal types, it is difficult to validate these keys with a type signature since it would lose specificity. Incorrect keys will result in errors when used (e.g., at a[key]
). There are workarounds available to ensure that only valid keys are present in keys
, such as creating a generic type that restricts its parameter to an array of valid keys:
type AssertKeys<K extends ReadonlyArray<keyof FidelityCheckRow>> = never
A dummy Assertion
type can be placed near the keys definition and passed the type of the keys tuple to use this approach:
const badKeys = ['P2', 'P3', 'P4'] as const
type Assertion = AssertKeys<typeof badKeys>
// ERROR: ... Type '"P4"' is not assignable to type 'keyof FidelityCheckRow'.
Another method involves using a constructor function to create the keys, acting as an identity function that limits its argument to valid key tuples:
const mkKeys = (ks: ReadonlyArray<keyof FidelityCheckRow>) => ks
If mkKeys
is utilized to generate 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'.
To further enhance this, the constructor can be parameterized with the object type and value type for versatility across various interfaces and value types:
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
Though the error messages may be less descriptive with this method, having explicit type parameters helps identify issues easily:
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