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:
tstype 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]}
tstype 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:
tstype RemoveIndexSignature<T, P = PropertyKey> = {[K in keyof T as P extends K ? never : (K extends P ? K : never)]: T[K]}
tstype 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:
tsP 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)
tsP 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.