July 30, 2018·6 min read·engineeringtypescriptbuildstypes

TypeScript 3.0 gives project dependencies an explicit build contract

Project references separate compilation units and let build mode order their dependencies. The new unknown type separately requires callers to establish what a value supports before using it.

Sharing source between a client and a server can make both builds absorb the shared files. The result can include duplicate compilation and an unexpected output-directory structure. TypeScript 3.0 introduces project references so a consumer can depend on another project's declared output without treating all of its source as part of the same compilation.

The July 30 announcement describes references as dependencies between TypeScript configurations. The compiler can use those relationships to establish build order and consult generated declaration files. This makes the compilation boundary an explicit part of project configuration, rather than an accidental consequence of which source files imports pull into a program.

I find TypeScript's web ecosystem impressive even while remaining ambivalent about the language. Explicit build boundaries help that ecosystem become easier to reason about. A shared library should have an identifiable output and dependency relationship, especially when browser and server programs both consume it.

References describe dependencies between configurations

A configuration can add a references array whose entries point to other configurations or directories containing them. The announcement pairs this with the composite option, which establishes the conditions needed for a project to be referenced and built incrementally.

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "../../lib/client",
    "strict": true,
    "module": "commonjs"
  },
  "references": [
    { "path": "../shared" }
  ]
}

This illustrative client configuration assumes a separately configured shared project at the referenced path. The shared project must itself satisfy the referenced-project requirements. Listing a directory as a dependency does not supply that project's configuration or turn arbitrary source into a completed library automatically.

Declaration output is central to the model. A dependent compilation can use the referenced project's .d.ts files for type information. That avoids importing the entire dependency's implementation into the consumer's compilation merely because the consumer uses its exports.

The announcement illustrates an incremental consequence with a dependency chain. A change in a lower-level project need not force every dependent project to rebuild if the relevant emitted declaration information has not changed. The dependency's implementation can change without changing the types its consumer needs to check against.

This separates implementation work from changes to the declared contract. It also gives build tools information they previously had to infer or duplicate. The project-reference APIs expose the relationships, allowing tools to schedule compilation with knowledge of which project supplies which declaration output.

Build mode follows the project graph

TypeScript 3.0 adds tsc --build, also available as tsc -b. It accepts projects and builds them with their dependencies. The announcement states that --build must appear first and documents accompanying options for inspecting or controlling the operation.

tsc --build src/client src/server --verbose

The paths identify configured projects. --verbose reports the build's steps. Other options include a dry run without output, cleaning known outputs and forcing a non-incremental rebuild. These are operations over the project relationships, so they differ from compiling an unrelated collection of source filenames together.

The client-and-server example explains the output benefit. Without an independent shared project, both consumers can compile the shared source and create extra nesting in their output directories. References identify a shared compilation with its own output, so each consumer no longer needs to emit another copy as part of its own program.

That does not make project references a complete JavaScript packaging or deployment system. The described mechanism establishes compilation ordering and the use of declaration outputs. A project still needs its runtime module paths and delivered artifacts to agree with its execution environment. A compiler dependency graph supplies information for that work without replacing every other build responsibility.

The release announcement also describes further work on editor integration. It presents seamless navigation and refactoring as an ongoing goal, with APIs available for tool authors. A July account should therefore distinguish the new compiler mechanism from a claim that every editor already supplies an identical cross-project experience.

unknown requires evidence before operations

The 3.0 release notes introduce unknown as a top type. Any value can be assigned to it. Unlike any, it does not permit arbitrary property access or calls merely because the program has not described the value more precisely.

function lengthIfText(value: unknown): number {
    if (typeof value === "string") {
        return value.length;
    }
    return 0;
}

The check narrows the value to string inside the branch, establishing why length is available there. Before that check, the declaration permits a value that might not support the operation. The type makes the absence of knowledge visible to the caller and implementation.

This is useful at an interface where a value's structure has not yet been established. Storing the value is permitted, while treating it as a specific object requires narrowing or an assertion. An assertion remains a programmer claim, so it does not perform runtime validation. The example uses an actual runtime test to establish its smaller contract.

The release notes also show assignment restrictions. An unknown value is assignable to unknown and any, but not automatically to a string, object or array. That prevents uncertainty from disappearing simply because the program passes the value to a more specific declaration.

The interaction with type operators is deliberate. keyof unknown is never, since an arbitrary value provides no known property names. In a union, unknown absorbs other ordinary types because it already covers their values. In an intersection, the other type supplies the available restriction. These rules let uncertainty participate consistently in generic declarations.

My preference is to use a declaration that requires evidence at the point of use when the input is genuinely unspecified. any can be necessary for particular interactions, but it also permits operations the declaration has supplied no reason to trust. unknown expresses a different agreement between the producer and consumer.

Tuples can describe parameter lists

Another part of TypeScript 3.0 improves higher-order function declarations. A generic rest parameter can capture a function's parameter list as a tuple and reuse it in a forwarding call. The announcement contrasts this with maintaining a finite series of overloads for different argument counts.

function invoke<A extends any[], R>(
    fn: (...args: A) => R,
    ...args: A
): R {
    return fn(...args);
}

const result = invoke((count: number, name: string) => name, 2, "batch");

The compiler can infer the argument tuple as a number followed by a string. Reusing that tuple connects the forwarded arguments to the function's expected parameters. It preserves information that an unrestricted any[] parameter would discard.

To support parameter lists, tuples gain optional elements, a trailing rest element and an empty form. An optional trailing element affects the possible lengths, so a tuple with one required and one optional member has a length type describing both possibilities. A tuple with an open-ended rest element instead has a general numeric length.

Those additions matter because function parameters are not always a fixed list of required values. Capturing only their element order would fail to preserve whether arguments can be omitted or repeated. TypeScript 3.0 extends the tuple model so the generic forwarding declaration can retain those properties of the original function.

The restriction to a trailing rest element belongs to this release's model. It should not be read as support for every possible arrangement of tuple segments. The concrete capability is capturing and reusing parameter lists through the tuple forms documented for TypeScript 3.0.

JSX checking can account for library-supplied properties

The release adds JSX.LibraryManagedAttributes, a declaration mechanism through which a JSX library can transform a component's property type before checking a JSX expression. The announcement uses React default properties as the motivating case. A component can require a property internally while allowing the caller to omit it because the library supplies a default.

This again distinguishes two contracts that otherwise look like one interface. The component's implementation can rely on a supplied value, while the JSX call site need not provide that value explicitly. Marking every property optional would weaken the implementation's information instead of describing the library's actual contribution.

The announcement includes an immediate availability qualification. React's declaration package does not yet take advantage of the new mechanism at publication time. Compiler support alone therefore does not establish that an installed combination of React declarations already provides the illustrated checking. A library needs to publish declarations using the new hook before its consumers receive that model. An overly broad annotation on default properties can also obscure which properties actually have defaults, so the information in the library declaration remains essential to the transformation.

Project references identify the compilation supplying a consumer's types, while unknown identifies where the consumer still needs evidence about a value. Generic tuple parameters preserve the argument relationship when one function forwards work to another. Each mechanism gives the compiler a specific contract it can use instead of relying on information lost during composition.