如何使用映射类型删除索引签名
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 string
但 string
不是 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 得到一个非常简洁的解决方案。
它的核心使用来自 的略微修改的逻辑:虽然已知键是 string
或 number
的子类型,但后者不是相应的子类型文字。另一方面,string
是所有可能字符串的并集(number
也是如此),因此是自反的(type res = string extends string ? true : false; //true
成立)。
这意味着每次类型 string
或 number
可分配给键的类型时,您都可以解析为 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; }
使用 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]
}
中试用
测试:
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 你可以通过使用一次处理一个字符的递归类型来检测一些无限的模板文字,但这不适用于长键。
给定接口(来自无法更改的现有 .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 string
但 string
不是 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 得到一个非常简洁的解决方案。
它的核心使用来自 string
或 number
的子类型,但后者不是相应的子类型文字。另一方面,string
是所有可能字符串的并集(number
也是如此),因此是自反的(type res = string extends string ? true : false; //true
成立)。
这意味着每次类型 string
或 number
可分配给键的类型时,您都可以解析为 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; }
使用 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]
}
中试用
测试:
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 你可以通过使用一次处理一个字符的递归类型来检测一些无限的模板文字,但这不适用于长键。