How to remove index signature using mapped types

Solution 1:

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

Solution 2:

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

Solution 3:

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.