14.5 Key Remapping with as Clauses
Alright, let’s talk about taking the keys you get from your mapped type and giving them a proper makeover. You’ve seen how to transform property values, but what if the key names themselves are the problem? That’s where key remapping with the as clause comes in. It’s the feature that takes mapped types from “usefully transformative” to “downright magical.”
The syntax looks a bit alien at first, but you’ll get used to it. Instead of just [K in KeyType]: ValueType, you write [K in KeyType as NewKeyType]: ValueType. The as NewKeyType part is where you remap the key. The magic is that NewKeyType can be a string literal type that’s derived from the original key K. This is how you escape the confines of the original object’s key set.
How the as Clause Bends Keys to Your Will
Think of it like this: you’re iterating over each key K in a union, KeyType. For each one, you can compute a new key name based on the old one. The most common and powerful way to do this is using template literal types.
Let’s say you have an object full of API endpoints, and some genius decided all the keys should be lowercase. You need to create a new type where each key is prefixed with get. Without key remapping, you’re manually constructing a new type. With it, it’s a one-liner.
type ApiEndpoints = {
user: (id: number) => Promise<User>;
product: (sku: string) => Promise<Product>;
category: (id: number) => Promise<Category>;
};
// Let's create a "getter" version dynamically
type ApiGetters = {
[K in keyof ApiEndpoints as `get${Capitalize<K>}`]: ApiEndpoints[K]
};
// This results in:
// type ApiGetters = {
// getUser: (id: number) => Promise<User>;
// getProduct: (sku: string) => Promise<Product>;
// getCategory: (id: number) => Promise<Category>;
// };
See what happened? We took each key (K), used the intrinsic type Capitalize<K> to make the first letter uppercase, and wrapped it in a template literal. The original key user became getUser. The property value type ApiEndpoints[K] remained unchanged. This is the bread and butter of key remapping.
The Powerhouse: Filtering Keys with never
This is arguably the coolest trick. The as clause doesn’t just let you rename keys; it lets you remove them entirely. How? If your remapping expression evaluates to never for a specific key K, that key is simply omitted from the resulting type. It’s a built-in filter.
This is how you finally create that OnlyNumberProperties<T> type you might have wondered about.
type DataConfig = {
id: number;
name: string;
score: number;
createdAt: Date;
};
// Filter: only keep properties where the original type is number
type NumericConfig = {
[K in keyof DataConfig as DataConfig[K] extends number ? K : never]: DataConfig[K]
};
// This results in:
// type NumericConfig = {
// id: number;
// score: number;
// };
The conditional type DataConfig[K] extends number ? K : never is the gatekeeper. For the key id, DataConfig['id'] is number, so the clause evaluates to 'id'. For the key name, DataConfig['name'] is string, so the clause evaluates to never. Poof. The name property is gone from the new type. This is infinitely more elegant and maintainable than using the Pick utility type with a manually constructed union.
Navigating the Rough Edges and Pitfalls
This power comes with a few sharp edges you need to watch for.
First, you can create key conflicts. What if two original keys remap to the same new key? TypeScript will happily let you do this, and the resulting type will have a property with the union of the two original value types. This is rarely what you actually want and is a fantastic way to introduce bizarre, hard-to-track bugs.
type ConflictingExample = {
[K in keyof ApiEndpoints as 'getEverything']: ApiEndpoints[K] // Don't do this.
};
// type ConflictingExample = {
// getEverything: ((id: number) => Promise<User>) | ((sku: string) => Promise<Product>) | ...;
// };
Second, remember that the as clause operates on types, not values. You’re working with the type K, not a string variable. This means you can use any type-level operations: template literals, conditional types, and even looking up other types.
// A more complex example: renaming based on the value type
type GettersAndSetters = {
[K in keyof DataConfig as `get${Capitalize<K>}`]: () => DataConfig[K];
} & {
[K in keyof DataConfig as `set${Capitalize<K>}`]: (value: DataConfig[K]) => void;
};
// This creates a type with getId(), setId(), getName(), setName(), etc.
The key remapping feature, especially when combined with conditional types and template literals, is what allows you to write truly dynamic and expressive type transformations. It moves TypeScript from merely describing your data to actively orchestrating it. Use it to build robust, type-safe utilities and keep your codebase DRY. Just be sure your powerful new key-generating machine doesn’t accidentally produce the same key twice.