Remove Index Signature
Remove Index Signature
题目
实现 RemoveIndexSignature<T>,从对象类型中排除索引签名。
type Foo = {
[key: string]: any
foo(): void
}
type A = RemoveIndexSignature<Foo> // 期望 { foo(): void }解法
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K]: T[K]
}核心技巧:过滤掉所有宽泛基础类型(string、number、symbol)能够 extends 的 key K。只有具体的具名 key 才能存活。
深入理解
什么是索引签名?
索引签名是一种"兜底"的 key 描述符,表示"这个对象可以存任意该形状的 key 下的值":
type WithIndex = {
[key: string]: any // ← 索引签名
foo(): void // ← 具名属性
}具名属性和索引签名在同一个类型里共存。难点在于:如何剥离签名,同时保留具体的属性名。
核心洞察:string extends K,而不是 K extends string
这是最关键的方向翻转。来想想每个 key K 长什么样:
| Key 类型 | string extends K? | K extends string? |
|---|---|---|
string(索引签名) | ✅ true | ✅ true |
'foo'(字面量) | ❌ false | ✅ true |
具名字符串 key 如 'foo' 是字符串字面量——它是 string 的子类型。但 string 不是 'foo' 的子类型。索引签名的 key 类型是完整的 string 类型,所以 string extends string 为 true,而 string extends 'foo' 为 false。
通过检测 string extends K,方向翻转:只有索引签名(K = string)才会产生 true,字面量 key 产生 false 并得以保留。
映射类型中的键重映射(as)
TypeScript 4.1 引入了通过 as 对映射类型进行键重映射:
type Remap<T> = {
[K in keyof T as SomeFilter<K>]: T[K]
}当 SomeFilter<K> 解析为 never 时,该键会被完全丢弃。我们利用这一点构造过滤器:
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never : K]: T[K]
// ^^^^^^^^^^^^^^^^
// "K 是 string 索引签名吗?" → 是则丢弃
}为什么要同时检查三个:string、number、symbol?
TypeScript 允许三种索引签名:
type AllIndexSigs = {
[key: string]: any // string 索引
[idx: number]: any // number 索引
[sym: symbol]: any // symbol 索引(TS 4.4+)
}三种都需要过滤:
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K]: T[K]
}用例子验证
type Foo = {
[key: string]: any
foo(): void
bar: number
}
type Result = RemoveIndexSignature<Foo>
// { foo(): void; bar: number }
// ✅ 索引签名被剔除,具名属性保留type Mixed = {
[idx: number]: string
length: number
push(item: string): void
}
type Result2 = RemoveIndexSignature<Mixed>
// { length: number; push(item: string): void }
// ✅ 数字索引被剔除为什么 PropertyKey extends K 不行
你可能想用 PropertyKey extends K 一步到位:
// ❌ 不行
type Bad<T> = {
[K in keyof T as PropertyKey extends K ? never : K]: T[K]
}PropertyKey 是 string | number | symbol。要让这个为 true,K 必须能容纳整个 PropertyKey 联合,但即使是 string 也无法以区分索引签名和字面量 key 的方式 extends string | number | symbol。必须逐一检测每个基础类型。
extends 方向性的优雅
这道题精妙地展示了 TypeScript 中 extends 的方向性:
A extends B= "A 可赋值给 B" = "A 是 B 的子类型"'foo' extends string→true(字面量是其基础类型的子类型)string extends 'foo'→false(基础类型更宽,不是子类型)
通过检测 string extends K 而不是 K extends string,我们只选出宽到足以成为索引签名本身的 key——而不是具体的具名 key。
核心要点
- 索引签名用宽泛类型(
string、number、symbol)作为 key;具名属性用字面量 string extends K过滤索引签名:只有K = string满足此条件;K = 'foo'不满足- 键重映射(
as) 让你通过映射到never来丢弃键 - 检查全部三种基础类型(
string、number、symbol)才能处理所有索引签名 extends方向至关重要:A extends B和B extends A语义相反
