Faster subsequent builds with the --incremental flag
TypeScript 3.4 introduces a new flag called incremental which tells TypeScript to save information about the project graph from the last compilation.
The next time TypeScript is invoked with incremental, it will use that information to detect the least costly way to type-check and emit changes to your project.
// tsconfig.json{"": {"": true,"": "./lib"},"": ["./src"]}
By default with these settings, when we run tsc, TypeScript will look for a file called .tsbuildinfo in the output directory (./lib).
If ./lib/.tsbuildinfo doesn’t exist, it’ll be generated.
But if it does, tsc will try to use that file to incrementally type-check and update our output files.
These .tsbuildinfo files can be safely deleted and don’t have any impact on our code at runtime - they’re purely used to make compilations faster.
We can also name them anything that we want, and place them anywhere we want using the tsBuildInfoFile option.
// front-end.tsconfig.json{"": {"": true,"": "./buildcache/front-end","": "./lib"},"": ["./src"]}
Composite projects
Part of the intent with composite projects (tsconfig.jsons with composite set to true) is that references between different projects can be built incrementally.
As such, composite projects will always produce .tsbuildinfo files.
outFile
When outFile is used, the build information file’s name will be based on the output file’s name.
As an example, if our output JavaScript file is ./output/foo.js, then under the incremental flag, TypeScript will generate the file ./output/foo.tsbuildinfo.
As above, this can be controlled with the tsBuildInfoFile option.
Higher order type inference from generic functions
TypeScript 3.4 can now produce generic function types when inference from other generic functions produces free type variables for inferences. This means many function composition patterns now work better in 3.4.
To get more specific, let’s build up some motivation and consider the following compose function:
tsfunction compose<A, B, C>(f: (arg: A) => B, g: (arg: B) => C): (arg: A) => C {return (x) => g(f(x));}
compose takes two other functions:
fwhich takes some argument (of typeA) and returns a value of typeBgwhich takes an argument of typeB(the typefreturned), and returns a value of typeC
compose then returns a function which feeds its argument through f and then g.
When calling this function, TypeScript will try to figure out the types of A, B, and C through a process called type argument inference.
This inference process usually works pretty well:
tsinterface Person {name: string;age: number;}function getDisplayName(p: Person) {return p.name.toLowerCase();}function getLength(s: string) {return s.length;}// has type '(p: Person) => number'const getDisplayNameLength = compose(getDisplayName, getLength);// works and returns the type 'number'getDisplayNameLength({ name: "Person McPersonface", age: 42 });
The inference process is fairly straightforward here because getDisplayName and getLength use types that can easily be referenced.
However, in TypeScript 3.3 and earlier, generic functions like compose didn’t work so well when passed other generic functions.
tsinterface Box<T> {value: T;}function makeArray<T>(x: T): T[] {return [x];}function makeBox<U>(value: U): Box<U> {return { value };}// has type '(arg: {}) => Box<{}[]>'const makeBoxedArray = compose(makeArray, makeBox);makeBoxedArray("hello!").value[0].toUpperCase();// ~~~~~~~~~~~// error: Property 'toUpperCase' does not exist on type '{}'.
In older versions, TypeScript would infer the empty object type ({}) when inferring from other type variables like T and U.
During type argument inference in TypeScript 3.4, for a call to a generic function that returns a function type, TypeScript will, as appropriate, propagate type parameters from generic function arguments onto the resulting function type.
In other words, instead of producing the type
ts(arg: {}) => Box<{}[]>
TypeScript 3.4 produces the type
ts<T>(arg: T) => Box<T[]>
Notice that T has been propagated from makeArray into the resulting type’s type parameter list.
This means that genericity from compose’s arguments has been preserved and our makeBoxedArray sample will just work!
tsinterface Box<T> {value: T;}function makeArray<T>(x: T): T[] {return [x];}function makeBox<U>(value: U): Box<U> {return { value };}// has type '<T>(arg: T) => Box<T[]>'const makeBoxedArray = compose(makeArray, makeBox);// works with no problem!makeBoxedArray("hello!").value[0].toUpperCase();
For more details, you can read more at the original change.
Improvements for ReadonlyArray and readonly tuples
TypeScript 3.4 makes it a little bit easier to use read-only array-like types.
A new syntax for ReadonlyArray
The ReadonlyArray type describes Arrays that can only be read from.
Any variable with a reference to a ReadonlyArray can’t add, remove, or replace any elements of the array.
tsfunction foo(arr: ReadonlyArray<string>) {arr.slice(); // okayarr.push("hello!"); // error!}
While it’s good practice to use ReadonlyArray over Array when no mutation is intended, it’s often been a pain given that arrays have a nicer syntax.
Specifically, number[] is a shorthand version of Array<number>, just as Date[] is a shorthand for Array<Date>.
TypeScript 3.4 introduces a new syntax for ReadonlyArray using a new readonly modifier for array types.
tsfunction foo(arr: readonly string[]) {arr.slice(); // okayarr.push("hello!"); // error!}
readonly tuples
TypeScript 3.4 also introduces new support for readonly tuples.
We can prefix any tuple type with the readonly keyword to make it a readonly tuple, much like we now can with array shorthand syntax.
As you might expect, unlike ordinary tuples whose slots could be written to, readonly tuples only permit reading from those positions.
tsfunction foo(pair: readonly [string, string]) {console.log(pair[0]); // okaypair[1] = "hello!"; // error}
The same way that ordinary tuples are types that extend from Array - a tuple with elements of type T1, T2, … Tn extends from Array< T1 | T2 | … Tn > - readonly tuples are types that extend from ReadonlyArray. So a readonly tuple with elements T1, T2, … Tn extends from ReadonlyArray< T1 | T2 | … Tn.
readonly mapped type modifiers and readonly arrays
In earlier versions of TypeScript, we generalized mapped types to operate differently on array-like types.
This meant that a mapped type like Boxify could work on arrays and tuples alike.
tsinterface Box<T> {value: T;}type Boxify<T> = {[K in keyof T]: Box<T[K]>;};// { a: Box<string>, b: Box<number> }type A = Boxify<{ a: string; b: number }>;// Array<Box<number>>type B = Boxify<number[]>;// [Box<string>, Box<number>]type C = Boxify<[string, boolean]>;
Unfortunately, mapped types like the Readonly utility type were effectively no-ops on array and tuple types.
ts// lib.d.tstype Readonly<T> = {readonly [K in keyof T]: T[K];};// How code acted *before* TypeScript 3.4// { readonly a: string, readonly b: number }type A = Readonly<{ a: string; b: number }>;// number[]type B = Readonly<number[]>;// [string, boolean]type C = Readonly<[string, boolean]>;
In TypeScript 3.4, the readonly modifier in a mapped type will automatically convert array-like types to their corresponding readonly counterparts.
ts// How code acts now *with* TypeScript 3.4// { readonly a: string, readonly b: number }type A = Readonly<{ a: string; b: number }>;// readonly number[]type B = Readonly<number[]>;// readonly [string, boolean]type C = Readonly<[string, boolean]>;
Similarly, you could write a utility type like Writable mapped type that strips away readonly-ness, and that would convert readonly array containers back to their mutable equivalents.
tstype Writable<T> = {-readonly [K in keyof T]: T[K];};// { a: string, b: number }type A = Writable<{readonly a: string;readonly b: number;}>;// number[]type B = Writable<readonly number[]>;// [string, boolean]type C = Writable<readonly [string, boolean]>;
Caveats
Despite its appearance, the readonly type modifier can only be used for syntax on array types and tuple types.
It is not a general-purpose type operator.
tslet err1: readonly Set<number>; // error!let err2: readonly Array<boolean>; // error!let okay: readonly boolean[]; // works fine
You can see more details in the pull request.
const assertions
TypeScript 3.4 introduces a new construct for literal values called const assertions.
Its syntax is a type assertion with const in place of the type name (e.g. 123 as const).
When we construct new literal expressions with const assertions, we can signal to the language that
- no literal types in that expression should be widened (e.g. no going from
"hello"tostring) - object literals get
readonlyproperties - array literals become
readonlytuples
ts// Type '"hello"'let x = "hello" as const;// Type 'readonly [10, 20]'let y = [10, 20] as const;// Type '{ readonly text: "hello" }'let z = { text: "hello" } as const;
Outside of .tsx files, the angle bracket assertion syntax can also be used.
ts// Type '"hello"'let x = <const>"hello";// Type 'readonly [10, 20]'let y = <const>[10, 20];// Type '{ readonly text: "hello" }'let z = <const>{ text: "hello" };
This feature means that types that would otherwise be used just to hint immutability to the compiler can often be omitted.
ts// Works with no types referenced or declared.// We only needed a single const assertion.function getShapes() {let result = [{ kind: "circle", radius: 100 },{ kind: "square", sideLength: 50 },] as const;return result;}for (const shape of getShapes()) {// Narrows perfectly!if (shape.kind === "circle") {console.log("Circle radius", shape.radius);} else {console.log("Square side length", shape.sideLength);}}
Notice the above needed no type annotations.
The const assertion allowed TypeScript to take the most specific type of the expression.
This can even be used to enable enum-like patterns in plain JavaScript code if you choose not to use TypeScript’s enum construct.
tsexport const Colors = {red: "RED",blue: "BLUE",green: "GREEN",} as const;// or use an 'export default'export default {red: "RED",blue: "BLUE",green: "GREEN",} as const;
Caveats
One thing to note is that const assertions can only be applied immediately on simple literal expressions.
ts// Error! A 'const' assertion can only be applied// to a string, number, boolean, array, or object literal.let a = (Math.random() < 0.5 ? 0 : 1) as const;let b = (60 * 60 * 1000) as const;// Works!let c = Math.random() < 0.5 ? (0 as const) : (1 as const);let d = 3_600_000 as const;
Another thing to keep in mind is that const contexts don’t immediately convert an expression to be fully immutable.
tslet arr = [1, 2, 3, 4];let foo = {name: "foo",contents: arr,} as const;foo.name = "bar"; // error!foo.contents = []; // error!foo.contents.push(5); // ...works!
For more details, you can check out the respective pull request.
Type-checking for globalThis
TypeScript 3.4 introduces support for type-checking ECMAScript’s new globalThis - a global variable that, well, refers to the global scope.
Unlike the above solutions, globalThis provides a standard way for accessing the global scope which can be used across different environments.
ts// in a global file:var abc = 100;// Refers to 'abc' from above.globalThis.abc = 200;
Note that global variables declared with let and const don’t show up on globalThis.
tslet answer = 42;// error! Property 'answer' does not exist on 'typeof globalThis'.globalThis.answer = 333333;
It’s also important to note that TypeScript doesn’t transform references to globalThis when compiling to older versions of ECMAScript.
As such, unless you’re targeting evergreen browsers (which already support globalThis), you may want to use an appropriate polyfill instead.
For more details on the implementation, see the feature’s pull request.