TypeScript 4.1 derives named APIs from existing object types
Template literal types and key remapping can describe getter and event conventions without duplicating a model. Recursive conditional types extend that work to nested containers, with explicit checking costs.
A library that turns a model into methods usually has to maintain two descriptions of the same relationship. The model contains a name property, while another declaration describes a getName method returning a string. TypeScript 4.1, released November 19, can derive that method name and return type from the original property. Template literal types construct the name, and key remapping places it into the resulting object type.
This is a substantial increase in what a declaration can describe. A library no longer has to weaken every generated name to string or enumerate every method manually. The compiler can preserve the relationship between an input property and an output operation. It still does not generate the runtime implementation of that operation.
I appreciate TypeScript's ability to describe conventions that already exist in web frameworks. The useful outcome is a clearer contract for the caller. A complicated type transformation needs to justify its cost through that contract, because declaration complexity can otherwise make a simple API harder to understand.
A mapped type can change the key it emits
Mapped types already construct object types by visiting keys. A declaration can make properties optional or change their value types while iterating over keyof T. The missing operation was a direct way to replace the output key with a name derived from the original one.
TypeScript 4.1 adds an as clause in that key position. The implementation of template literal types and key remapping supplies the compiler representation needed for these transformations. A mapped entry can now connect its input key to a differently named output member.
The following getter type follows the pattern in the release announcement. The concrete object demonstrates an implementation satisfying the resulting declaration, rather than claiming that the type creates the methods.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Model {
name: string;
workers: number;
}
const accessors: Getters<Model> = {
getName: () => "queue",
getWorkers: () => 4
};
const count: number = accessors.getWorkers();
For each property, the mapped type retains the original key as K while computing another key for the output. The value side still indexes the original model through T[K]. Changing the output name therefore does not require losing the relationship to the original property type.
The intersection with string deliberately limits the name construction to string keys. The getter convention is a string convention. It should not silently imply the same spelling transformation for every kind of key the broader object model permits.
Casing helpers operate on literal types
The stable release provides Uppercase, Lowercase, Capitalize and Uncapitalize as utility type aliases. The first pair transforms every character, while the second pair changes the initial character. That gives a getter convention a specific operation for changing name into the suffix used by getName.
These utilities belong in the type expression. They do not call a JavaScript string method during execution or modify the model's stored property name. An implementation constructing runtime method names still needs to implement the corresponding naming rule.
That distinction is easy to miss because template literal types use familiar backtick syntax. The same visual form appears in ordinary JavaScript expressions, but its position determines whether it describes a type or computes a runtime string. In the mapped declaration, it constructs the permitted member name for checking.
The resulting relationship can eliminate duplicated declarations when a model changes. A new property contributes another derived member, and a changed property type changes that member's result type. The implementation then has to satisfy the updated declaration. This is useful precisely because it makes disagreement visible at the boundary between generated conventions and their implementation.
Filtering can happen in the same mapping
The as clause can also remove an output key by producing never. Excluding a discriminant named kind while retaining the other properties is the release's example. The original key remains available to select each retained property's value type.
type WithoutKind<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
const dimensions: WithoutKind<Circle> = { radius: 3 };
This example describes a type without the discriminant. It does not delete a field from an existing runtime object. A runtime transformation that promises to remove data must still perform that operation separately.
The distinction matters for API design. A filtered declaration can define which fields a particular function expects or returns. It cannot serve as evidence that a serialization routine omitted a field. The compiler models the shape that the code claims to expose, while the implementation determines the actual data.
I prefer transformations like this when they reduce a real source of duplicated knowledge. Filtering one key and preserving the other values has a clear purpose. Adding layers of remapping without a corresponding runtime convention would make the declaration more difficult without giving the caller a more useful guarantee.
Inference can recover a property from an event name
Template literal types also work in the other direction. The release's watched-object example accepts names such as nameChanged. A generic method can infer the property key from the part preceding Changed, then use that key to determine the callback's value type.
That gives event registration a relationship between two arguments. Selecting the name event supplies a string payload, while selecting a numeric property supplies a numeric payload. A declaration accepting every event name with a union of every payload type would lose that correlation.
The property-change pattern is therefore more useful than an independently enumerated list of spellings. It can adapt to a caller's model, reject an unknown property name and preserve the selected property's value. The emitted event system still needs to deliver values according to the same convention.
This feature also has a size cost. Unions in substitution positions produce combinations of their members. The announcement recommends considering ahead-of-time generation or a simpler string type when a declaration would construct a very large collection. A derived API should preserve useful information without constructing possibilities the application never needs.
Recursive conditional types describe nested containers
The release also permits conditional types to refer to themselves immediately inside their branches. That makes recursive transformations of nested containers easier to write. An element type can unwrap another array until it reaches a value that is not an array.
type ElementType<T> =
T extends ReadonlyArray<infer U> ? ElementType<U> : T;
type Leaf = ElementType<readonly (readonly number[])[]>;
const leaf: Leaf = 7;
The recursive branch extracts the array's element type and applies the same operation again. The final branch retains the value type when no further array layer matches. This declaration models a nested-type relationship. It does not flatten a runtime array by itself.
The announcement uses the same capability to show a user-defined type that unwraps promise-like values. That example is a type an author can write with the new feature. It should not be read as a promise that every similarly named utility already exists in the standard declarations or handles every possible asynchronous abstraction.
Recursive evaluation consumes compiler work and can reach an internal depth limit. The release explicitly warns that sufficiently complex inputs can produce a compile-time error. That limit is part of choosing what a published declaration can reasonably ask every consumer's checker to evaluate.
Indexed access can preserve absence too
Another feature in 4.1 addresses a different kind of overstatement. With noUncheckedIndexedAccess, an access resolved through an index signature includes the possibility of undefined. An index signature's broad name domain no longer implies that every possible key has a stored value.
Array indexing also becomes stricter, including inside a bounds-checked loop as described in the announcement. Iterating directly over elements can avoid the uncertain indexed lookup when the index itself is unnecessary. The option is separate from strict, so a project selects that checking policy explicitly.
The connection with derived names is the precision of the declaration's claim. A finite set derived from actual model keys can describe a known relationship. An arbitrary lookup into a broadly indexed object may still be absent. More expressive types are useful when they preserve those differences.
The release makes related configuration choices less coupled as well. Path mappings no longer require setting baseUrl, whose additional bare-specifier resolution behaviour could affect auto-import suggestions. A project can request the mapping without also requesting that broader resolution rule. Checked JavaScript setup also becomes less repetitive because checkJs implies allowJs by default. These are smaller changes than key remapping, but both make a configuration express its intended operation with fewer incidental settings. They affect how a project reaches the checker, while the new type operations determine what its declarations can describe.
For a getter API, TypeScript 4.1 can now keep the method name and result type tied to the original property. Key remapping selects the output name, the template literal constructs its spelling, and T[K] retains the value relationship. The concrete accessor object must still provide methods satisfying that derived contract.