TypeScript 2.8 expresses relationships between input and output types
Conditional types let libraries describe results that depend on their inputs. Distribution over unions and inference within type patterns explain both their usefulness and their limits.
A function can choose its result shape from its input, while its declaration exposes only a union of every possible result. That loses information at the call site. Overloads can preserve the relationship, but repeating them across a library duplicates the same decision. TypeScript 2.8 introduces conditional types so the relationship itself can be named and reused.
The March 27 announcement describes the new form as a type-level assignability test. T extends U ? X : Y chooses one branch when T is assignable to U and the other when it is not. When the result depends on an unresolved type parameter, the compiler can defer the decision until more information becomes available.
I remain cautious about TypeScript's complexity, but its ecosystem produces useful web interfaces when the types fit the framework's semantics. Conditional types can improve that fit. They let library declarations preserve relationships that already exist in JavaScript instead of making every caller reconstruct them with assertions.
One reusable declaration can preserve a result choice
Consider a library operation that accepts either a number identifying an item or a string naming it. The announcement uses this kind of example to show why a small overload set becomes repetitive. A conditional result alias can express the choice once.
interface NumberLabel {
id: number;
}
interface TextLabel {
name: string;
}
type Label<T extends number | string> =
T extends number ? NumberLabel : TextLabel;
declare function label<T extends number | string>(value: T): Label<T>;
const byId = label(7);
const byName = label("queue");
The numeric call has a numeric-label result, and the string call has a text-label result. A call whose input is itself a union can retain a union result. The relationship lives in Label<T>, so other declarations can use it without copying the overload list.
This declaration describes behaviour that an implementation must actually provide. It does not insert the runtime branch. A program still needs JavaScript code that constructs the appropriate object. A precise type declaration is useful because callers can reason about that implementation's result, not because the declaration performs the work itself.
The distinction also matters to library maintainers reviewing a change. The input-to-output relationship is part of the public contract. Changing the implementation's choice without changing the conditional declaration can leave checked callers relying on a result shape the implementation no longer supplies.
Distribution explains what happens to unions
The 2.8 release notes give a precise condition for distribution. When the checked type is a bare type parameter, instantiating it with a union applies the conditional to the union's members individually. The resulting branch types then form a union.
type SeparateArrays<T> = T extends any ? T[] : never;
type CombinedArray<T> = [T] extends [any] ? T[] : never;
type Separate = SeparateArrays<string | number>;
type Combined = CombinedArray<string | number>;
Separate becomes a union of string arrays and number arrays. Combined becomes an array whose element type is the string-or-number union. Wrapping the compared types prevents the bare-parameter distribution. The two results differ in whether one array can mix both element types.
That is a semantic distinction a library can need to preserve. A union of homogeneous containers does not express the same thing as a container of union values. Reading the condition without checking whether it distributes can hide this difference even when the generic alias looks compact.
Distribution also explains how conditional types filter unions. A branch that yields never contributes no surviving member to the result. The built-in Exclude<T, U> removes members of T assignable to U, while Extract<T, U> keeps those members. Their usefulness follows from combining the per-member test with the way never behaves in the resulting union.
These utilities filter types, not runtime collections. Exclude<string | number, string> changes a type expression. It does not examine data or reject a string received from an external source. Runtime validation and compile-time descriptions remain separate responsibilities.
Inference matches a type's structure
The infer keyword introduces a type variable inside a conditional's comparison pattern. The true branch can then refer to the inferred component. An example in the announcement extracts an array element type through Array<infer U>.
type Item<T> = T extends Array<infer U> ? U : T;
type Names = Item<string[]>;
type Count = Item<number>;
The compiler infers U as string in the first case. The second comparison does not match an array, so its false branch leaves number unchanged. This is one level of inspection, not a request to flatten every nested structure recursively.
The release notes explicitly prohibit a conditional type from referencing itself recursively in this release. That limit matters when interpreting examples. A type that extracts one layer can be applied again explicitly, but it cannot be presented as an unrestricted recursive alias in TypeScript 2.8.
The same pattern mechanism supports function-result extraction. ReturnType<T> inspects a function type and infers its return component. The built-in alias makes that relationship available without each library defining a private equivalent.
There is an important limit for overloaded functions. The notes say inference uses the last call signature, normally the general catch-all signature. It does not perform overload resolution against an imagined argument list. A return-type query therefore cannot be assumed to recover whichever overload result a particular call expression would select.
This limitation keeps the distinction between inspecting a declaration and checking a call. The former has a set of signatures available as type information. The latter also has actual argument types against which overload selection can operate. Conditional inference supplies pattern matching within types, not a replacement for every part of call checking.
Multiple inference sites also have defined combination rules. The release notes demonstrate two properties whose values both contribute candidates for one inferred variable. Candidates from covariant positions produce a union, so a string in one property and a number in another can yield a string-or-number result. When candidates instead come from contravariant positions, the notes show an intersection result. The position of an inferred variable therefore affects how the compiler combines the evidence it obtains.
This matters when moving a type pattern between an ordinary property and a function parameter. Similar-looking patterns can have different relationships to assignability. A utility author needs to understand the relationship being described instead of assuming that repeating an inferred name always means selecting a common union. The examples make the inference rule inspectable through small declarations whose results can be stated directly.
The built-in helpers are also intentionally selective. The notes explain that the release does not add a built-in Omit, since the operation can be expressed with Pick<T, Exclude<keyof T, K>>. That composition first removes the unwanted names from the key union and then selects the remaining properties. It illustrates the practical purpose of the new conditional operation, supplying a reusable step in an existing type transformation instead of requiring a separate compiler feature for each derived interface.
Mapped types can remove modifiers
TypeScript 2.8 also extends mapped types with explicit modifier removal. Earlier mapped types could add readonly and optionality, but could not directly remove those modifiers. The new minus-prefixed form lets a transformation describe a mutable or required version of an existing property set.
type MutableRequired<T> = {
-readonly [P in keyof T]-?: T[P];
};
This preserves the field names while changing the selected modifiers. The built-in Required<T> uses removal of the optional marker to make every property required. Under strict null checking, the documented homomorphic transformation also removes the optional property's associated undefined.
Here too, transforming a type does not transform an object. Making a property required in a type alias does not supply its missing value. The utility is appropriate when an implementation has already established the stronger object contract and needs to describe it consistently.
Conditional types and modifier control can be combined with mapped property selection. The release notes show selecting property names according to whether their value types are functions, then using that set to construct a related object type. This lets a library derive declarations from one source type instead of maintaining parallel interfaces by hand.
My judgement is that this is useful when the derived type removes duplication the reader would otherwise maintain. The added notation still needs a clear relationship to the JavaScript operation. A compact alias that hides an unrelated collection of assumptions can increase the effort required to understand an interface.
Declaration output can be a separate build product
The release's emitDeclarationOnly option generates declaration files without JavaScript or source maps. The announcement identifies alternate JavaScript compilers as a use case. A package can therefore obtain its .d.ts description from TypeScript while another step owns JavaScript generation.
That separation makes accurate declarations more consequential. Conditional types give a package more expressive ways to describe relationships, while declaration-only emission provides a direct way to deliver those descriptions independently. The package still needs its emitted JavaScript and its declarations to agree about the input-to-output relationships that callers will rely upon.