TypeScript 2.1 derives related types from object properties
TypeScript 2.1 combines keyof, indexed access, and mapped types so libraries can express property-dependent APIs without repeating every field.
A function that reads a property needs to relate two inputs. The property name must belong to the object, and the result type depends on which property was requested. Describing the name merely as a string loses that relationship. Describing the result as a union of every possible property value is safe but discards useful precision. TypeScript 2.1 introduces the type operators needed to express the relationship directly.
The December 7 release announcement presents keyof, indexed access types, and mapped types together. Their combined effect is larger than another collection of predefined utility aliases. The compiler can derive names and corresponding value types from an existing object type, then use that information to build other types. A library declaration can describe how its inputs relate instead of enumerating every application-specific property.
I remain cautious about the complexity TypeScript can introduce, but this is useful complexity for web applications. Component state and object-oriented library interfaces often need precisely these relationships. Expressing them once in a declaration can make the application code using that declaration simpler and more accurately checked.
Keys become a type-level result
For an interface with a fixed set of named properties, keyof produces a union of those names. The announcement calls this an index type query. A type with name, age, and location properties therefore supplies the corresponding literal-name union without a separately maintained list.
interface Person {
name: string;
age: number;
location: string;
}
let property: keyof Person;
property = "age";
The relationship is structural. Adding another property to Person changes the query result because the keys come from the type itself. A manually written union can drift away from the object it is intended to describe. The query eliminates that particular duplication while retaining a finite set of permitted property names.
Indexed access types provide the corresponding value operation. Person["age"] is the type of the age property, which is number here. Indexing with a union such as "name" | "age" produces a union of the corresponding property types. The syntax resembles value-level element access, but the operation takes place in a type expression.
This distinction matters when reading a declaration. A type query does not retrieve a value from a particular object at runtime. It describes the values permitted by the type. The runtime implementation must still perform the actual property access, while the declaration tells the checker how that access relates to its arguments.
A generic lookup preserves the selected property
The release announcement combines the operators in a generic getter. A simplified version retains the same relationship between object, key, and result.
function get<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
let person = { name: "Ada", age: 36 };
let age = get(person, "age");
let personName = get(person, "name");
Here K must be one of the keys of T. The result is T[K], so the first call produces a number type and the second a string type. A name absent from the object is rejected. The implementation does not need a separate overload for every possible property name or a declaration specific to Person.
That is a useful improvement over returning any. A caller can continue to receive precise checking after accessing a property through a helper function. It is also more precise than returning all property types for every call. The selected key carries information that the return type can preserve.
The declaration has a readable explanation despite its generic syntax. One parameter identifies the object's type, another identifies a permitted property, and the result refers to that property's type. The mechanism is worth understanding because libraries can use it to describe operations whose correctness depends on a relationship between arguments.
Mapped types transform the property set
Mapped types generate properties by iterating over a union of literal types. The announcement starts with a type whose properties all become booleans. Substituting keyof Person for a handwritten name union keeps that transformation connected to the original interface. Generalizing the source to T gives a reusable transformation.
The property value can also depend on the current key. T[P] selects the original property's type while modifiers change how the property can be used. That permits an all-optional version or a read-only version without restating each field.
type OptionalFields<T> = {
[P in keyof T]?: T[P];
};
type ReadonlyFields<T> = {
readonly [P in keyof T]: T[P];
};
The transformation applies to the properties selected at this level. It does not recursively rewrite every nested object merely because an outer property becomes read-only. The announcement itself motivates the read-only example through a shallow freeze. Distinguishing a property's assignment rule from the mutability of an object referenced by that property avoids expecting more from the type than the declaration expresses.
Optional properties also describe permitted shapes, without implementing an update operation. A partial object can be used as input to an API that applies updates, but the API decides what those updates mean. The mapped type supplies the static relationship between the original fields and the allowed input fields.
Utility aliases replace a special operator
The team initially planned a dedicated partial operator. The announcement identifies React's state updates as a motivating use for making every property optional. Mapped types make that special operator unnecessary because an ordinary generic type alias can express the transformation. TypeScript 2.1 supplies Partial and Readonly as standard utilities, alongside Record and Pick.
That design choice is significant for library authors. A built-in operation solves the particular transformation its designer chose. A general mapping mechanism also permits a library to describe transformations appropriate to its own API. The standard utilities demonstrate the mechanism while remaining type aliases that developers can inspect.
I favor that result when the general rule remains smaller than the duplicated declarations it replaces. Repeating a model's properties in update and read-only variants creates several places to make an inconsistent edit. A mapped declaration keeps those variants tied to the model. It does require reviewers to understand the transformation, which is easier when libraries name it according to its actual purpose.
Selecting properties preserves their original types
The utility declarations shipped with 2.1 show how Pick combines the same operations. Its key parameter is constrained by keyof T, and the mapped result uses T[P] for each selected property. A selection therefore keeps the value types of the source properties. Selecting name and age does not turn them into one common type, since the lookup runs separately for each mapped key.
Record expresses a different relationship. It receives a set of string keys and one value type, then maps every selected key to that type. This is appropriate when the names define a fixed collection of entries that all share a value representation. It does not need an existing object type from which to obtain the values. The distinction between Pick and Record follows from which information each transformation receives.
These declarations give reviewers small examples of type-level composition. A union determines the property names, a mapping creates the resulting properties, and indexed access is used where the original values must be preserved. Understanding the inputs makes it possible to distinguish a transformation that retains a model's field types from one that deliberately replaces them.
Object spread has a related runtime role
The release includes object rest and spread, which the announcement describes as proposal syntax. Spread creates a shallow copy or combines properties from several objects. Later properties take precedence over earlier ones, including a later spread replacing a property explicitly written earlier in the literal. Rest extracts the properties not already selected during destructuring.
Those are runtime operations, unlike a mapped type. A spread expression can construct a new value while a mapped type describes a related set of properties for checking. Both are useful around updates, but neither substitutes for the other. The type declaration does not copy data, and a copy expression does not by itself specify the generic relationship a library promises to callers.
The release also retains the inference and downlevel async work described in the November release-candidate announcement. For async functions, the final announcement says the compiler's emit pipeline was rewritten around tree transforms while preserving existing output behavior and emission performance. That work enables the older syntax targets, while the property-type changes improve the account of APIs that the compiler checks before emission.
One additional default affects the quality of that account. TypeScript 2.1 permits importing an installed package without declarations and treats it as any, unless noImplicitAny requests the older diagnostic. This reduces the initial friction of using a package, but the useful property relationships described above depend on having type information to query. The announcement recommends noImplicitAny for new projects. An installed implementation can run without declarations, while precise generic checking needs declarations that describe its interface.