← back

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 type
type Data = {
name: string,
[key: string]: number;
};
// we want to get
type Expected = {
name: string
}
ts
// given the following type
type Data = {
name: string,
[key: string]: number;
};
// we want to get
type 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.