November 29, 2018·6 min read·engineeringtypescripttypesbigint

TypeScript 3.2 checks indirect calls and separates BigInt arithmetic

Strict bind, call and apply checking preserves function parameter relationships through indirect invocation. BigInt support introduces a distinct numeric type and an explicit runtime-target requirement.

A function's declared parameters can be precise while an indirect call through apply() accepts the wrong argument list. Earlier TypeScript declarations for function methods were too permissive to preserve the original signature. TypeScript 3.2 uses the parameter-list machinery introduced in 3.0 to check those indirect invocations more closely.

The November 29 announcement introduces strictBindCallApply. It combines explicit this parameter types with generic rest parameters to describe these JavaScript methods. The same release adds BigInt checking, another case where a permissive approximation would hide a meaningful runtime distinction.

I am ambivalent about TypeScript but value its role in the web ecosystem. A check is useful when it carries an existing contract through an ordinary JavaScript operation. Indirect function calls should not discard the argument information that made a direct call safe to check.

apply can preserve the original parameter list

The 3.2 release notes show the new checking rejecting too few arguments, too many arguments and a wrong argument type. A correct invocation retains the original return type.

function describe(count: number, name: string): string {
    return count + " " + name;
}

const text = describe.apply(undefined, [2, "batches"]);

Under strictBindCallApply, the second argument must match the function's parameter list. Supplying a number in the string position is an error, as is omitting the name. The array passed to apply() is no longer an unexamined route around the direct-call contract.

The implementation uses new global declarations named CallableFunction and NewableFunction. Their generic methods describe callable functions and constructable functions respectively. These declarations replace the permissive Function methods for the stricter mode.

That mechanism matters because the compiler does not need a special hard-coded rule for every application function. It can capture the function's parameter tuple and use it in the method declaration. The earlier work on tuples as parameter lists makes this checking expressible through the type system.

bind() also relates one invocation to another. Binding the receiver and some arguments produces a function whose remaining arguments must still fit the original declaration. A result typed as any would discard that relationship, while the stricter declarations retain useful information for subsequent calls.

The flag belongs to the strict family, so projects already enabling strict mode can receive new diagnostics. That is a compatibility change in what the compiler accepts, even though the JavaScript methods have not changed their runtime behaviour. The newly rejected call was already inconsistent with the function declaration.

Generic and overloaded functions retain limits

The release explicitly qualifies the new checking. Generic functions and overloaded functions are not fully modeled by these methods. Generic type parameters are substituted with the empty object type, and overloaded functions use the final overload.

This matters when a library uses an overloaded method as a compact public interface. Calling it directly and obtaining its apply() member are different checking situations in this release. A successful direct call can have information that the indirect-method declaration does not preserve.

The qualification also prevents an overstatement about universal type safety. The release makes common indirect calls stricter using existing abstractions. It does not claim that every relationship expressible in a generic or overloaded signature survives binding and application unchanged.

For a library author, that is a concrete limitation to understand when choosing an exposed call shape. A declaration should be assessed through the way consumers actually invoke it, including any indirect call methods, instead of assuming that one precise direct-call signature answers every use case.

BigInt introduces a separate numeric domain

TypeScript 3.2 adds the primitive bigint type and accepts BigInt literals when targeting esnext. The announcement describes BigInt as an upcoming ECMAScript proposal for integers that can grow beyond the ordinary number representation's range.

const quantity: bigint = 100n;
const scaled = quantity * 10n;

function kind(value: number | bigint): string {
    if (typeof value === "bigint") {
        return "integer";
    }
    return "number";
}

The lowercase type name differs from the BigInt() conversion function. The trailing n marks a literal in the new numeric domain. A typeof check produces the string bigint, allowing TypeScript to narrow a union accordingly.

Numbers and bigints are not mutually assignable. Mixing them in arithmetic is also an error under the described ECMAScript behaviour. A numeric value needs an explicit conversion when an operation requires a bigint. The checker therefore preserves the distinction instead of treating BigInt as a wider version of every existing number.

This is useful because a program must decide where conversion belongs. Silently accepting mixed arithmetic would obscure an operation that has different runtime rules. A declaration distinguishing the two domains lets the compiler identify that decision at the expression where the values meet.

The type does not, by itself, make a runtime support BigInt. TypeScript accepts and checks the construct under the supported target, while the executing environment must implement its semantics. The announcement identifies Node 11 and newer Chrome versions as available supporting environments at publication time.

Older targets would need operator rewriting

The release supports BigInt emission only for esnext and has no immediate plan to lower it for older targets. The announcement explains that arithmetic operators would need to dispatch according to their operands. Replacing a literal or constructor name alone cannot provide the full behaviour.

This is an important limit on the usual expectation that TypeScript can translate new syntax for older browsers. BigInt changes what arithmetic operations mean for a primitive value. Supporting that semantics in an older runtime would require transformations throughout expressions where those values participate.

The esnext.bigint library declaration can describe relevant APIs for environments supplying an implementation. It does not implement those APIs or change the semantics of the runtime's existing operators. As with other declaration libraries, it tells the checker what the project assumes is present.

My preference is for that target requirement to remain visible in the build. The declaration and emitted syntax can be correct while an older browser still lacks the runtime capability. Treating the language version, compiler output target and delivered environment as explicit inputs makes the remaining responsibility understandable.

Generic object spread uses an approximation

TypeScript 3.2 also permits spreading generic objects and models the result with intersection types. The announcement explains that a dedicated spread-type operator was considered, but would require additional relationship and inference rules. The project chose an existing construct that already models common Object.assign() cases.

function combine<T, U>(first: T, second: U) {
    return { ...first, ...second };
}

The inferred relationship is T & U. This is useful for non-conflicting properties, but the release notes identify a difference when the inputs declare the same property with incompatible types. Ordinary known-object spread uses the rightmost property's type, while an intersection combines the property types.

That is a genuine modeling tradeoff. The JavaScript operation still gives a later property precedence. The generic declaration uses an intersection as its approximation, so the type result can differ from the result inferred when both object types are directly visible. A helper author should not describe it as an exact type-level implementation of every overwrite case.

Generic object rest uses another existing composition. Removing named properties yields a type built from Pick and Exclude, selecting the original keys after excluding the destructured names. That keeps the remaining property relationship available without inventing a new rest-type operator.

Configuration can expose the effective checking policy

Another checking improvement allows certain non-unit property types to participate in union discrimination. The notes show a result union with an error object in one variant and a null error in the success variant. Checking the shared error property can now narrow the containing object even though Error itself is not a singleton type. The rule requires a singleton among the property alternatives and excludes generics from the discriminant property.

This supports a common representation without requiring an additional string tag solely for the checker. The data field can retain its relationship to success or failure through the existing error property. The improvement concerns what the compiler can infer from the branch, rather than changing the representation's runtime behaviour.

JavaScript checking also recognizes Object.defineProperty() declarations. The announcement demonstrates a string-valued, non-writable property and diagnostics for a misspelled string method and an attempted reassignment. That gives another established JavaScript declaration pattern a type model, extending useful feedback to checked JavaScript without requiring a conversion to TypeScript syntax first.

The release adds --showConfig, which prints the effective configuration after resolving inherited options. It also permits a bare package path in extends, resolving a shared configuration through node_modules. The resolver can use the package's tsconfig field or its root configuration file.

These additions matter when stricter indirect-call checking comes through a shared base configuration. The leaf file may not spell out every option that controls diagnostics. Printing the effective configuration makes the inherited policy available for inspection, while package-based inheritance provides a way to distribute that policy across independently packaged projects.