Removing Index Signature type
This post is my explanation of a solution to the following TS challenge mostly for archival purposes.
Problem statement
Given an object type, we are asked to remove index signature keys from it, i.e:
ts
// given the following typetype Data = {name: string,[key: string]: number;};// we want to gettype Expected = {name: string}
ts
// given the following typetype Data = {name: string,[key: string]: number;};// we want to gettype Expected = {name: string}
Solution
The main observation here is that explicitly written keys inside type = { ... }
are literal types, whereas index signature keys are string | number | symbol
. All we need to do now is differentiate between keys that are literal
types or supertypes thereof. Notice that 'Hello World' extends string
but
string extends 'Hello World'
is not the case.
The more verbose solution would be:
ts
type RemoveIndexSignature<T> = {[K in keyof T as/* filters out all 'string' keys */string extends K? never/* filters out all 'number' keys */: number extends K? never/* filers out all 'symbol' keys */: symbol extends K? never: K /* all that's left are literal type keys */]: T[K]}
ts
type RemoveIndexSignature<T> = {[K in keyof T as/* filters out all 'string' keys */string extends K? never/* filters out all 'number' keys */: number extends K? never/* filers out all 'symbol' keys */: symbol extends K? never: K /* all that's left are literal type keys */]: T[K]}
the above could be shortened into the following, yielding @alexfung888’s solution:
ts
type RemoveIndexSignature<T, P = PropertyKey> = {[K in keyof T as P extends K ? never : (K extends P ? K : never)]: T[K]}
ts
type RemoveIndexSignature<T, P = PropertyKey> = {[K in keyof T as P extends K ? never : (K extends P ? K : never)]: T[K]}
the main trick here is to distribute over PropertyKey
by storing it in a generic variable P = PropertyKey
since extends
distributes over only naked types, P extends PropertyKey
wouldn’t work. I.e:
ts
P extends K ? never : (K extends P ? K : never) /* P = string | number | symbol */// becomes(string | number | symbol) extends K ? never : (K extends P ? K : never)// becomes| string extends K ? never : (K extends string ? K : never)| number extends K ? never : (K extends number ? K : never)| symbol extends K ? never : (K extends symbol ? K : never)
ts
P extends K ? never : (K extends P ? K : never) /* P = string | number | symbol */// becomes(string | number | symbol) extends K ? never : (K extends P ? K : never)// becomes| string extends K ? never : (K extends string ? K : never)| number extends K ? never : (K extends number ? K : never)| symbol extends K ? never : (K extends symbol ? K : never)
after substituting K = "somePropertyName"
you can see how only literal types are preserved.