typescriptmapped-types

How to remove index signature using mapped types


Given an interface (from an existing .d.ts file that can't be changed):

interface Foo {
  [key: string]: any;
  bar(): void;
}

Is there a way to use mapped types (or another method) to derive a new type without the index signature? i.e. it only has the method bar(): void;


Solution

  • 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.