TypeScript 4.0 preserves argument lists through partial application
Variadic tuples describe the supplied and remaining arguments of a function in one signature. Labeled elements improve the resulting API without adding runtime validation.
A wrapper can forward the right JavaScript arguments while losing almost all useful information about them in its declaration. Partial application makes the problem obvious. Supply the first argument now, return a function for the rest, and the type needs to describe exactly which parameters remain. TypeScript 4.0, released August 20, adds variadic tuple types that express this relationship without enumerating each supported function length.
The release combines generic tuple spreads with improved inference for rest parameters. Those changes let a declaration split a parameter list into a supplied prefix and a remaining suffix. Labeled tuple elements then make those positions easier to explain in tooling. The runtime still forwards ordinary JavaScript arguments, while the checker retains more of their contract.
I appreciate TypeScript's framework ecosystem most when abstractions remain understandable at their call sites. A helper should preserve the function's argument requirements instead of making its users reconstruct them from an implementation. This feature gives library authors a way to describe that preservation directly.
Overloads describe instances of a more general operation
The announcement begins with array concatenation because the runtime implementation is simple. It spreads two inputs into a new array. Older precise declarations can enumerate empty tuples, one-element tuples and progressively longer tuples, but each signature only describes another particular shape.
A catch-all array signature accepts more inputs while losing tuple positions. An array of the union of element types does not say which type belongs at each index or how many values exist. The implementation's ordering guarantee survives at runtime, but the declaration no longer exposes it.
Variadic tuples express the operation on element sequences. A generic spread can stand for a tuple whose actual elements will be supplied through inference. Concatenating two sequences becomes a return type containing one spread followed by the other. The declaration describes a rule instead of a list of arities.
The variadic tuple implementation adds explicit handling of variable tuple elements in the checker. That is the mechanism behind the source spelling. The feature requires the compiler to reason about positions and lengths during substitution and inference, not merely accept another punctuation pattern.
Partial application splits one parameter list
The release's partial-application example uses two generic sequences. The function being wrapped accepts their concatenation. Arguments supplied immediately determine the prefix, and the returned function accepts the remaining sequence. A separate result parameter preserves the return type.
The following helper follows that design. Its ordinary implementation concatenates the captured and later arguments when invoking the original function.
type Arguments = readonly unknown[];
function partialCall<T extends Arguments, U extends Arguments, R>(
operation: (...args: [...T, ...U]) => R,
...head: T
) {
return (...tail: U) => operation(...head, ...tail);
}
function describe(name: string, workers: number, enabled: boolean): string {
return `${name} ${workers} ${enabled}`;
}
const describeQueue = partialCall(describe, "queue");
const result: string = describeQueue(4, true);
Here the supplied name accounts for the first parameter. The returned function still requires a number followed by a boolean. Its result remains a string, because the helper relates its result to the original operation through R.
The important information is what the wrapper does not discard. The remaining parameters are not reduced to an arbitrary array of a broad type. Supplying too few values or a string in the boolean position can therefore produce an error at the wrapper's call site.
The initial call is checked as well. A number cannot stand in for the original function's leading string parameter. Supplying arguments beyond the original list also conflicts with the declared relationship. The helper does not make arbitrary prefixes valid just because its own implementation uses rest syntax.
Runtime forwarding still has to match the declaration
Variadic tuples do not verify a separate hand-written runtime contract independently of the implementation. The helper's source still matters. In this example it forwards the captured prefix before the later suffix, matching the concatenation in its parameter type.
Reordering values or discarding them would be a different operation. A declaration for the original order would then misdescribe the implementation. Better inference reduces the need for broad escape types, which makes it easier to keep that relationship visible to the checker and to reviewers.
The feature also does not introduce runtime validation of JavaScript callers. Once emitted, the helper forwards values using JavaScript spreads. Consumers that do not undergo checking can still pass incompatible values. TypeScript's declaration describes and checks supported calls during compilation.
I prefer this boundary stated explicitly. Stronger declarations can prevent mistakes within checked application code, and they improve editor assistance. They do not replace validation where an application accepts external values whose types are not established by the compiler.
Exact positions depend on exact inputs
The release distinguishes tuples of known length from arrays whose length is unknown. Spreading a fixed tuple gives the checker a known sequence of positions. Spreading an unbounded array cannot supply the same positional information.
TypeScript 4.0 permits generic tuple spreads in positions that previously required the rest element to appear last. That makes combinations of fixed sequences possible. When an unbounded array participates, the release explains that following elements can become part of the resulting rest element type.
This limit follows from the information available in the input type. A runtime array might happen to contain two numbers, but a declaration of number[] does not promise that length. A wrapper cannot derive a fixed suffix boundary merely by hoping its caller uses a particular array size.
The concatenation example in the release uses readonly array constraints so callers can supply readonly tuples. That acceptance rule is separate from the returned array's mutability. A function constructing a new result can accept an input it does not mutate without promising that the new result is readonly too.
For library design, these distinctions determine which relationships the generic should preserve. Exact positions, readonly inputs and newly allocated outputs are separate properties. Variadic syntax gives them a common expression, but the author still chooses which properties the API guarantees.
Tuple labels make parameter roles visible
Labels address a different problem. A tuple such as [number, number] has precise element types but does not explain what the two numbers mean. TypeScript 4.0 permits a declaration such as [start: number, end: number] to retain the role of each position in documentation and tooling.
Those names do not make the tuple an object with named properties. The values remain positional. A caller can destructure them into variables with different names, and compatibility does not depend on preserving the labels.
The release requires all elements in a labeled tuple to have labels. Optional and rest elements use syntax corresponding to parameter lists, so a tuple-based rest parameter can communicate a function-like signature. The editor can display suitable tuple unions as overloads where possible.
That is useful when a library describes several valid argument sequences. The representation can preserve each sequence's positional contract while the editor gives the caller names for those positions. It reduces the readability cost of implementing an API through tuple transformations.
The release also reduces annotation work inside classes
Constructor-based property inference is another example of retaining information already present in the code. With noImplicitAny enabled, TypeScript 4.0 can infer class-property types from assignments in the constructor. A property assigned a number does not need a duplicate annotation solely to avoid an implicit-any result.
Control flow still matters. The announcement says a property remains potentially undefined if some constructor paths do not assign it. Moving initialization into a separate method can also require an explicit annotation and definite-assignment assertion under strict property initialization. The compiler's inference has a defined scope.
The connection with tuple inference is practical. In both cases, the compiler can preserve a relationship that previously required more manual declaration work. It also retains uncertainty when the source does not establish a stronger fact. An unbounded argument array does not become a fixed tuple, and a conditionally initialized property does not become definitely assigned.
The release makes another uncertainty explicit by allowing unknown annotations on catch variables. The default remains any, but an explicit unknown annotation requires narrowing before operations such as calling a string method. That gives error-handling code a way to avoid assuming what was thrown. It is a separate feature with the same useful discipline of preserving missing information. A type should not claim a known operation when the input's declaration has not established that operation is available.
For partial application, the resulting API is precise about the boundary the helper creates. The first call must supply a valid prefix of the original parameters. The returned function requires the remaining sequence and preserves the original result type. Tuple labels can explain the visible positions without changing those checking rules.