TypeScript 4.1 beta connects event-name strings to property types
Template literal types can build event names from object keys and infer those keys back from a supplied string. The beta also introduces mapped-type key remapping.
A property-change API often encodes a relationship in a string. An event named workersChanged refers to the workers property, and its callback should receive that property's value type. Before this relationship can help a caller, the declaration needs to represent it. The TypeScript 4.1 beta announced September 18 adds template literal types that can construct such names and infer information back from them.
The useful change goes beyond rejecting spelling mistakes in a fixed list. A generic API can derive valid names from the object supplied by its caller. It can then connect the selected name with the corresponding callback parameter, avoiding an independently maintained list of event strings and payload types.
I appreciate this capability for web application APIs whose conventions already depend on strings. TypeScript becomes more useful when it can describe the convention the library actually implements. The declaration still needs to remain understandable enough that the relationship is visible to the caller.
String construction becomes a type operation
The beta uses JavaScript template-literal syntax in a type position. Substituting a concrete string literal produces another literal type. Substituting a union produces the set of combinations described by that union.
For a small fixed API, a union can still be written directly. The announcement explicitly acknowledges that generating a large collection of strings during checking can be more work than generating declarations ahead of time. The stronger use case is a relationship whose inputs come from another type.
Anders Hejlsberg's implementation of string templates and key remapping adds compiler handling for that representation. The type system can preserve literal text and substituted types rather than treating the whole result as an unconstrained string.
The following declaration derives property-change event names and callback values from one model. It describes an existing source of events, so it does not pretend to implement notification delivery.
type Changes<T> = {
on<K extends string & keyof T>(
event: `${K}Changed`,
callback: (value: T[K]) => void
): void;
};
interface Model {
workers: number;
name: string;
}
declare const changes: Changes<Model>;
changes.on("workersChanged", value => {
console.log(value.toFixed(0));
});
changes.on("nameChanged", value => {
console.log(value.toUpperCase());
});
The event argument gives inference a way to choose K. For workersChanged, matching the suffix leaves workers, and indexing the model with that key produces number. The name event similarly selects string. The callback parameter follows the selected event instead of becoming a union of every property type.
The key restriction belongs to the declaration
The intersection with string limits this event convention to string keys. The declaration is deliberately describing names constructed by appending text to a property key. It should not imply that every kind of object key participates in that convention.
A misspelled event or a property name without the suffix does not match the declared form. That catches a different error from invoking a string method on the numeric payload. Both checks follow from the same relationship between K, the event name and T[K].
This remains compile-time information. The declaration does not arrange for a changed value to reach listeners, and it does not validate arbitrary strings arriving from an external source. An implementation and its runtime input checks still have to satisfy the API being described.
Mapped types can transform property names too
The beta also adds an as clause for mapped-type keys. A mapped type no longer has to retain the original key unchanged. It can construct another key from it or remove the entry by producing never.
That supports related APIs that expose generated method names or filter fields from a model. The declaration can retain the value relationship through T[K] while changing how the caller names the operation. A property rename can therefore change the generated declaration through the same source type.
The beta's casing helpers have an important version-specific form. Its announcement describes lowercase type operators used inside substitution positions, with a proposed switch to named aliases still in progress. Uppercase and lowercase operations transform all characters, while capitalization operations affect the first character. A generated getter convention can use that distinction to preserve the rest of a property name. A beta example should not assume that proposed syntax is already available. The event example above avoids that changing detail while exercising string construction and inference.
I prefer starting with a relationship this small before adding more transformations. The type's purpose remains legible, and the callback has a concrete improvement in precision. A chain of string manipulations that obscures the library's actual contract would make the declaration harder to maintain.
A declaration can encode a convention without implementing it
The central benefit is one source for the property and its event payload type. Adding or changing a property changes the corresponding permitted event and callback relationship. It removes duplicated type declarations without requiring a new runtime event mechanism.
For a library already emitting property-change notifications, the beta can describe those notifications more precisely. Event-name construction identifies the property, and inference recovers that property to determine the callback value. Runtime notification delivery remains the implementation's separate responsibility.