如何使用映射类型删除索引签名

How to remove index signature using mapped types

给定接口(来自无法更改的现有 .d.ts 文件):

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

有没有办法在没有索引签名的情况下使用映射类型(或其他方法)来派生新类型?即它只有方法 bar(): void;

不是真的。你不能 "subtract" 像这样的界面。每个成员都是 public,任何声称实现 Foo 的人都必须实现它们。通常,您只能通过 extends 声明合并 扩展接口,但不能从中删除内容。

没有真正通用的方法,但如果您知道需要哪些属性,则可以使用 Pick:

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

type FooWithOnlyBar = Pick<Foo, 'bar'>;

const abc: FooWithOnlyBar = { bar: () => { } }

abc.notexisting = 5; // error

编辑: 自 Typescript 4.1 以来,有一种方法可以直接使用 Key Remapping, avoiding the Pick combinator. Please see .

type RemoveIndex<T> = {
  [ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
};

这是基于 'a' extends stringstring 不是 extends 'a' 的事实。


还有一种方法可以用 TypeScript 2.8 来表达 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>>;

你可以用它做一个通用的:

// Generic !!!
type RemoveIndex<T extends Record<any,any>> = Pick<T, KnownKeys<T>>;

type FooWithOnlyBar = RemoveIndex<Foo>;

有关 KnownKeys<T> 为何有效的解释,请参阅以下答案:

使用 TypeScript v4.1 key remapping 得到一个非常简洁的解决方案。

它的核心使用来自 的略微修改的逻辑:虽然已知键是 stringnumber 的子类型,但后者不是相应的子类型文字。另一方面,string 是所有可能字符串的并集(number 也是如此),因此是自反的(type res = string extends string ? true : false; //true 成立)。

这意味着每次类型 stringnumber 可分配给键的类型时,您都可以解析为 never,有效地过滤掉它:

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

使用 TypeScript 4.4,该语言获得了对更复杂的索引签名的支持。

interface FancyIndices {
  [x: symbol]: number;
  [x: `data-${string}`]: string
}

symbol 键可以通过在先前发布的类型中为其添加大小写来轻松捕获,但这种检查方式无法检测无限的模板文字。1

但是,我们可以通过修改检查以查看使用每个键构造的对象是否可分配给空对象来实现相同的目标。这是可行的,因为“真正的”键将要求用 Record<K, 1> 构造的对象具有 属性,因此将不可分配,而作为索引签名的键将导致一个类型可能只包含空对象。

type RemoveIndex<T> = {
  [K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}

playground

中试用

测试:

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 你可以通过使用一次处理一个字符的递归类型来检测一些无限的模板文字,但这不适用于长键。