Remove Index Signature
Remove Index Signature
Challenge
Implement RemoveIndexSignature<T>, which excludes index signatures from object types.
type Foo = {
[key: string]: any
foo(): void
}
type A = RemoveIndexSignature<Foo> // expected { foo(): void }Solution
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K]: T[K]
}The trick: filter out any key K where a broad primitive (string, number, or symbol) extends K. Only concrete named keys survive.
Deep Dive
What is an index signature?
An index signature is a catch-all key descriptor that says "this object can hold values at any key of this shape":
type WithIndex = {
[key: string]: any // ← index signature
foo(): void // ← named property
}Named properties and index signatures coexist in the same type. The challenge is to strip the signatures while keeping the concrete names.
The key insight: string extends K, not K extends string
This is the critical inversion. Let's think about what each key K looks like:
| Key type | string extends K? | K extends string? |
|---|---|---|
string (index sig) | ✅ true | ✅ true |
'foo' (literal) | ❌ false | ✅ true |
Named string keys like 'foo' are string literals — they're subtypes of string. But string is not a subtype of 'foo'. The index signature's key type is the full string type, so string extends string is true, while string extends 'foo' is false.
By testing string extends K, we flip the direction: only index signatures (where K = string) will produce true. Literal keys produce false and survive the filter.
Mapped type remapping with as
TypeScript 4.1 introduced key remapping in mapped types via as:
type Remap<T> = {
[K in keyof T as SomeFilter<K>]: T[K]
}When SomeFilter<K> resolves to never, the key is dropped entirely. We exploit this to build a filter:
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K ? never : K]: T[K]
// ^^^^^^^^^^^^^^^^
// "is K the string index signature?" → drop if yes
}Why check all three: string, number, symbol?
TypeScript allows three kinds of index signatures:
type AllIndexSigs = {
[key: string]: any // string index
[idx: number]: any // number index
[sym: symbol]: any // symbol index (TS 4.4+)
}We must filter all three. The full check:
type RemoveIndexSignature<T> = {
[K in keyof T as string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K]: T[K]
}Verifying with examples
type Foo = {
[key: string]: any
foo(): void
bar: number
}
type Result = RemoveIndexSignature<Foo>
// { foo(): void; bar: number }
// ✅ Index signature stripped, named properties preservedtype Mixed = {
[idx: number]: string
length: number
push(item: string): void
}
type Result2 = RemoveIndexSignature<Mixed>
// { length: number; push(item: string): void }
// ✅ Numeric index strippedWhy PropertyKey extends K won't work
You might think to use PropertyKey extends K:
// ❌ Doesn't work
type Bad<T> = {
[K in keyof T as PropertyKey extends K ? never : K]: T[K]
}PropertyKey is string | number | symbol. For this to be true, K would need to encompass all of PropertyKey, but even string doesn't extend the union string | number | symbol in a way that would distinguish index sigs from literals. You must check each primitive individually.
The elegance of extends directionality
This challenge beautifully illustrates that extends in TypeScript is directional:
A extends B= "A is assignable to B" = "A is a subtype of B"'foo' extends string→true(literals are subtypes of their base)string extends 'foo'→false(the base is wider, not narrower)
By checking string extends K instead of K extends string, we select only keys broad enough to be the index signature itself — not the concrete named keys.
Key Takeaways
- Index signatures use broad types (
string,number,symbol) as keys; named properties use literals string extends Kfilters index signatures: onlyK = stringsatisfies this;K = 'foo'does not- Key remapping (
as) in mapped types lets you drop keys by mapping them tonever - Check all three primitives (
string,number,symbol) to handle every kind of index signature - The direction of
extendsmatters:A extends BvsB extends Ahave opposite semantics
