How to remove index signature using mapped types
Edit: Since Typescript 4.1 there is a way of doing this directly with Key Remapping, avoiding the Pick
combinator. Please see the answer by Oleg where it's introduced.
type RemoveIndex<T> = {
[ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
};
It is based on the fact that 'a' extends string
but string
doesn't extends 'a'
.
There is also a way to express that with TypeScript 2.8's Conditional Types.
interface Foo {
[key: string]: any;
[key: number]: any;
bar(): void;
}
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
type FooWithOnlyBar = Pick<Foo, KnownKeys<Foo>>;
You can make a generic out of that:
// Generic !!!
type RemoveIndex<T extends Record<any,any>> = Pick<T, KnownKeys<T>>;
type FooWithOnlyBar = RemoveIndex<Foo>;
For an explanation of why exactly KnownKeys<T>
works, see the following answer:
https://stackoverflow.com/a/51955852/2115619
With TypeScript 4.4, the language gained support for more complex index signatures.
interface FancyIndices {
[x: symbol]: number;
[x: `data-${string}`]: string
}
The symbol
key can be trivially caught by adding a case for it in the previously posted type, but this style of check cannot detect infinite template literals.1
However, we can achieve the same goal by modifying the check to see if an object constructed with each key is assignable to an empty object. This works because "real" keys will require that the object constructed with Record<K, 1>
have a property, and will therefore not be assignable, while keys which are index signatures will result in a type which may contain only the empty object.
type RemoveIndex<T> = {
[K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}
Try it out in the playground
Test:
class X {
[x: string]: any
[x: number]: any
[x: symbol]: any
[x: `head-${string}`]: string
[x: `${string}-tail`]: string
[x: `head-${string}-tail`]: string
[x: `${bigint}`]: string
[x: `embedded-${number}`]: string
normal = 123
optional?: string
}
type RemoveIndex<T> = {
[K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}
type Result = RemoveIndex<X>
// ^? - { normal: number, optional?: string }
1 You can detect some infinite template literals by using a recursive type that processes one character at a time, but this doesn't work for long keys.
With TypeScript v4.1 key remapping leads to a very concise solution.
At its core it uses a slightly modified logic from Mihail's answer: while a known key is a subtype of either string
or number
, the latter are not subtypes of the corresponding literal. On the other hand, string
is a union of all possible strings (the same holds true for number
) and thus is reflexive (type res = string extends string ? true : false; //true
holds).
This means you can resolve to never
every time the type string
or number
is assignable to the type of key, effectively filtering it out:
interface Foo {
[key: string]: any;
[key: number]: any;
bar(): void;
}
type RemoveIndex<T> = {
[ P in keyof T as string extends P ? never : number extends P ? never : P ] : T[P]
};
type FooWithOnlyBar = RemoveIndex<Foo>; //{ bar: () => void; }
Playground