Key remapping with as
Renames or filters the keys a mapped type produces by computing a new key per property.
{ [Property in keyof Type as NewKeyType]: Type[Property] }- Added in TypeScript 4.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$Property | type variable name | The original key, bound per iteration and usable inside the as clause. |
$NewKeyType | type | The computed key, which must be string, number or symbol. Usually a template literal type, an Exclude call, or a conditional type. Evaluating to never drops the property. |
Return value
An object type whose keys are the computed NewKeyType valuesA new object type. Each key is replaced by whatever NewKeyType evaluates to for that property; a property whose computed key is never is omitted entirely.
Errors
A computed key that is not assignable to string | number | symbol is an error: "Type 'X' is not assignable to type 'string | number | symbol'".
Changelog
- PHP 4.1Introduced together with template literal types and the Uppercase, Lowercase, Capitalize and Uncapitalize intrinsics.
- PHP 4.1Adding an as clause makes the mapping non-homomorphic, so array and tuple inputs are no longer preserved as arrays and existing modifiers are not passed through automatically.
By default a mapped type keeps the key it was given. An as clause lets you compute a different one. [K in keyof T as \`get${Capitalize<string & K>}\`] turns name into getName, and because the clause is an ordinary type expression it can do anything type-level: prefix, suffix, case-change, or decide the key based on the property's value type.
Dropping keys
Mapping a key to never removes the property. That is the whole mechanism — there is no separate delete operator. as Exclude<K, "kind"> drops one named key; as T[K] extends Function ? never : K drops every method and leaves the data.
When to reach for it
Deriving an API surface from a data shape: getters, setters, on*Changed handler maps, snake_case-to-camelCase conversions of a server payload. Also for the negative case — stripping internal fields off a type before it crosses a boundary.
What trips people up
Capitalize<K> alone fails. keyof T can include number and symbol, and the case intrinsics only accept strings, so you need string & K to narrow first. That intersection in every example is not decoration.
Second, an as clause makes the mapping non-homomorphic: arrays stop coming out as arrays and modifiers are no longer inherited automatically.
Example
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type RemoveKind<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
interface Circle { kind: "circle"; radius: number; label: string }
type CircleGetters = Getters<Circle>;
// { getKind: () => "circle"; getRadius: () => number; getLabel: () => string }
type Kindless = RemoveKind<Circle>;
// { radius: number; label: string }When to use it
- Generate a getters interface from a data model so accessor names cannot drift
- Strip a discriminant or an internal field off a type before serialising it
- Convert a snake_case server payload type into camelCase for the client
Frequently asked questions
How do I remove a property in a mapped type?
Map its key to never in the as clause. [K in keyof T as Exclude<K, "kind">] drops one named key, and [K in keyof T as Condition ? never : K] drops every key matching a condition. There is no separate omit syntax at the mapping level.
Why does Capitalize<K> give an error inside an as clause?
Because keyof T may include number and symbol keys, and the case intrinsics only accept string. Narrow it first with an intersection: Capitalize<string & K>.
Does key remapping work on arrays and tuples?
It works, but adding an as clause makes the mapping non-homomorphic, so an array input no longer produces an array and modifiers are not carried over automatically. If you need array preservation, map without as and transform elsewhere.
More examples
Filtering by value type, not just by name
The as clause receives a conditional type rather than a string, so the decision to keep a property can depend on its value type. Producing never removes the property, and producing K keeps it unchanged - the two aliases are the same expression with the branches swapped. Public shows the same trick matching on the key's shape with a template literal pattern.
// keep only the data properties, drop every method
type DataOnly<T> = {
[K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]: T[K];
};
// keep only the methods
type MethodsOnly<T> = {
[K in keyof T as T[K] extends (...args: never[]) => unknown ? K : never]: T[K];
};
interface Model {
id: number;
title: string;
save(): void;
reload(): Promise<void>;
}
type ModelData = DataOnly<Model>; // { id: number; title: string }
type ModelApi = MethodsOnly<Model>; // { save(): void; reload(): Promise<void> }
// drop keys that look private
type Public<T> = { [K in keyof T as K extends `_${string}` ? never : K]: T[K] };
type P = Public<{ id: number; _secret: string }>; // { id: number }Mapping over a union of objects
Here the mapping iterates a union of object types rather than a set of keys, and each member supplies its own key by way of its discriminant. The result is a handler table where every handler is typed to exactly the event that reaches it, so adding a new event type to the union forces a new entry in Config.
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
};
type SquareEvent = { kind: "square"; x: number; y: number };
type CircleEvent = { kind: "circle"; radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>;
// {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
// }
// setters, the mirror of getters
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type PersonSetters = Setters<{ name: string; age: number }>;
// { setName: (value: string) => void; setAge: (value: number) => void }
Discussion