TypeScript 3.0, our next release of the type system, compiler, and language service, is fast-approaching! Today we’re excited to announce the Release Candidate of TypeScript 3.0! We’re looking to get any and all feedback from this RC to successfully ship TypeScript 3.0 proper, so if you’d like to give it a shot now, you can get the RC through NuGet, or use npm with the following command:
npm install -g typescript@rc
You can also get editor support by
- Downloading for Visual Studio 2017 (for version 15.2 or later)
- Following directions for Visual Studio Code and Sublime Text.
While Visual Studio 2015 doesn’t currently have an RC installer, TypeScript 3.0 will be available for Visual Studio 2015 users.
While you can read about everything on our Roadmap, we’ll be discussing a few major items going into TypeScript 3.0.
- Project references
- Extracting and spreading parameter lists with tuples
- Richer tuple types
- The
unknown
type - Support for React’s
defaultProps
Without further ado, let’s jump into some highlights of the Release Candidate!
Project references
It’s fairly common to have several different build steps for a library or application. Maybe your codebase has a src
and a test
directory. Maybe you have your front-end code in a folder called client
, your Node.js back-end code in a folder called server
, each which imports code from a shared
folder. And maybe you use what’s called a “monorepo” and have many many projects which depend on each other in non-trivial ways.
One of the biggest features that we’ve worked on for TypeScript 3.0 is called “project references”, and it aims to make working with these scenarios easier.
Project references allow TypeScript projects to depend on other TypeScript projects – specifically, allowing tsconfig.json
files to reference other tsconfig.json
files. Specifying these dependencies makes it easier to split your code into smaller projects, since it gives TypeScript (and tools around it) a way to understand build ordering and output structure. That means things like faster builds that work incrementally, and support for transparently navigating, editing, and refactoring across projects. Since 3.0 lays the foundation and exposes the APIs, any build tool should be able to provide this.
What’s it look like?
As a quick example, here’s what a tsconfig.json
with project references looks like:
// ./src/bar/tsconfig.json
{
"compilerOptions": {
// Needed for project references.
"composite": true,
"declaration": true,
// Other options...
"outDir": "../../lib/bar",
"strict": true, "module": "esnext", "moduleResolution": "node",
},
"references": [
{ "path": "../foo" }
]
}
There are two new fields to notice here: composite
and references
.
references
simply specifies other tsconfig.json
files (or folders immediately containing them). Each reference is currently just an object with a path
field, and lets TypeScript know that building the current project requires building that referenced project first.
Perhaps equally important is the composite
field. The composite
field ensures certain options are enabled so that this project can be referenced and built incrementally for any project that depends on it. Being able to intelligently and incrementally rebuild is important, since build speed is one of the reasons you might break up a project in the first place. For example, if project front-end
depends on shared
, and shared
depends on core
, our APIs around project references can be used to detect a change in core
, but to only rebuild shared
if the types (i.e. the .d.ts
files) produced by core
have changed. That means a change to core
doesn’t completely force us to rebuild the world. For that reason, setting composite
forces the declaration
flag to be set as well.
--build
mode
TypeScript 3.0 will provide a set of APIs for project references so that other tools can provide this fast incremental behavior. As an example, gulp-typescript already does support it! So project references should be able to integrate with your choice of build orchestrators in the future.
However, for many simple apps and libraries, it’s nice not to need external tools. That’s why tsc
now ships with a new --build
flag.
tsc --build
(or its nickname, tsc -b
) takes a set of projects and builds them and their dependencies. When using this new build mode, the --build
flag has to be set first, and can be paired with certain other flags:
--verbose
: displays every step of what a build requires--dry
: performs a build without emitting files (this is useful with--verbose
)--clean
: attempts to remove output files given the inputs--force
: forces a full non-incremental rebuild for a project
Controlling output structure
One subtle but incredibly useful benefit of project references is logically being able to map your input source to its outputs.
If you’ve ever tried to share TypeScript code between the client and server of your application, you might have run into problems controlling the output structure.
For example, if client/index.ts
and server/index.ts
both reference shared/index.ts
for the following projects:
src
├── client
│ ├── index.ts
│ └── tsconfig.json
├── server
│ ├── index.ts
│ └── tsconfig.json
└── shared
└── index.ts
…then trying to build client
and server
, we’ll end up with…
lib
├── client
│ ├── client
│ │ └── index.js
│ └── shared
│ └── index.js
└── server
├── server
│ └── index.js
└── shared
└── index.js
rather than
lib
├── client
│ └── index.js
├── shared
│ └── index.js
└── server
└── index.js
Notice that we ended up with a copy of shared
in both client
and server
. We unnecessarily spent time building shared
and introduced an undesirable level of nesting in lib/client/client
and lib/server/server
.
The problem is that TypeScript greedily looks .ts
files and tries to include them in a given compilation. Ideally, TypeScript would understand these files don’t need to be built in the same compilation, and instead jump to the .d.ts
files for type information.
Creating a tsconfig.json
for shared
and using project references does exactly that. It signals to TypeScript that
shared
should be built independently, and that- when importing from
../shared
, we should look for the.d.ts
files in its output directory.
This avoids triggering a double-build, and also avoids accidentally absorbing all the contents of shared
.
Further work
To get a deeper understanding of project references and how you can use them, read up more our issue tracker. In the near future, we’ll have documentation on project references and build mode.
We’re committed to ensuring that other tool authors can support project references, and continuing to improve the experience around editor support using
Extracting and spreading parameter lists with tuples
We often take it for granted, but JavaScript lets us think about parameter lists as first-class values – either by using arguments
or rest-parameters (e.g. ...rest
).
function call(fn, ...args) {
return fn(...args);
}
Notice here that call works on functions of any parameter length. Unlike other languages, we don’t need to define a call1
, call2
, call3
as follows:
function call1(fn, param1) {
return fn(param1);
}
function call2(fn, param1, param2) {
return fn(param1, param2);
}
function call3(fn, param1, param2, param3) {
return fn(param1, param2, param3);
}
Unfortunately, for a while there wasn’t a great well-typed way to express this statically in TypeScript without declaring a finite number of overloads:
// TODO (billg): 4 overloads should *probably* be enough for anybody?
function call<T1, T2, T3, T4, R>(fn: (param1: T1, param2: T2, param3: T3, param4: T4) => R, param1: T1, param2: T2, param3: T3, param4: T4);
function call<T1, T2, T3, R>(fn: (param1: T1, param2: T2, param3: T3) => R, param1: T1, param2: T2, param3: T3);
function call<T1, T2, R>(fn: (param1: T1, param2: T2) => R, param1: T1, param2: T2);
function call<T1, R>(fn: (param1: T1) => R, param1: T1): R;
function call(fn: (...args: any[]) => any, ...args: any[]) {
fn(...args);
}
Oof! Another case of death by a thousand overloads! Or at least, as many overloads as our users asked us for.
TypeScript 3.0 allows us to better model scenarios like these by now allowing rest parameters to be generic, and inferring those generics as tuple types! Instead of declaring each of these overloads, we can say that the ...args
rest parameter from fn
must be a type parameter that extends an array, and then we can re-use that for the ...args
that call
passes:
function call<TS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R {
return fn(...args);
}
When we call the call
function, TypeScript will try to extract the parameter list from whatever we pass to fn
, and turn that into a tuple:
function foo(x: number, y: string): string {
return (x + y).toLowerCase();
}
// `TS` is inferred as `[number, string]`
call(foo, 100, "hello");
When TypeScript infers TS
as [number, string]
and we end up re-using TS
on the rest parameter of call
, the instantiation looks like the following
function call(fn: (...args: [number, string]) => string, ...args: [number, string]): string
And with TypeScript 3.0, using a tuple in a rest parameter gets flattened into the rest of the parameter list! The above boils down to simple parameters with no tuples:
function call(fn: (arg1: number, arg2: string) => string, arg1: number, arg2: string): string
So in addition to catching type errors when we pass in the wrong arguments:
function call<TS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R {
return fn(...args);
}
call((x: number, y: string) => y, "hello", "world");
// ~~~~~~~
// Error! `string` isn't assignable to `number`!
and inference from other arguments:
call((x, y) => { /* .... */ }, "hello", 100);
// ^ ^
// `x` and `y` have their types inferred as `string` and `number` respectively.
we can also observe the tuple types that these functions infer from the outside:
function tuple<TS extends any[]>(...xs: TS): TS {
return xs;
}
let x = tuple(1, 2, "hello"); // has type `[number, number, string]
There is a subtler point to note though. In order to make all of this work, we needed to expand what tuples could do…
Richer tuple types
Parameter lists aren’t just ordered lists of types. Parameters at the end can be optional:
// Both `y` and `z` are optional here.
function foo(x: boolean, y = 100, z?: string) {
// ...
}
foo(true);
foo(true, undefined, "hello");
foo(true, 200);
And the final parameter can be a rest parameter.
// `rest` accepts any number of strings - even none!
function foo(...rest: string[]) {
// ...
}
foo();
foo("hello");
foo("hello", "world");
Finally, there is one mildly interesting property about parameter lists which is that they can be empty:
// Accepts no parameters.
function foo() {
// ...
}
foo()
So to make it possible for tuples to correspond to parameter lists, we needed to model each of these scenarios.
First, tuples now allow trailing optional elements:
/**
* 2D, or potentially 3D, coordinate.
*/
type Coordinate = [number, number, number?];
The Coordinate
type creates a tuple with an optional property named 2
– the element at index 2
might not be defined! Interestingly, since tuples use numeric literal types for their length
properties, Coordinate
‘s length
property has the type 2 | 3
.
Second, tuples now allow rest elements at the end.
type OneNumberAndSomeStrings = [number, ...string[]];
Rest elements introduce some interesting open-ended behavior to tuples. The above OneNumberAndSomeStrings
type requires its first property to be a number
, and permits 0 or more string
s. Indexing with an arbitrary number
will return a string | number
since the index won’t be known. Likewise, since the tuple length won’t be known, the length
property is just number
.
Of note, when no other elements are present, a rest element in a tuple is identical to itself:
type Foo = [...number[]]; // Equivalent to `number[]`.
Finally, tuples can now be empty! While it’s not that useful outside of parameter lists, the empty tuple type can be referenced as []
:
type EmptyTuple = [];
As you might expect, the empty tuple has a length
of 0
and indexing with a number
returns the never
type.
The unknown
type
The any
type is the most-capable type in TypeScript – while it encompasses the type of every possible value, it doesn’t force us to do any checking before we try to call, construct, or access properties on these values. It also lets us assign values of type any
to values that expect any other type.
This is mostly useful, but it can be a bit lax.
let foo: any = 10;
// All of these will throw errors, but TypeScript
// won't complain since `foo` has the type `any`.
foo.x.prop;
foo.y.prop;
foo.z.prop;
foo();
new foo();
upperCase(foo);
foo `hello world!`;
function upperCase(x: string) {
return x.toUpperCase();
}
There are often times where we want to describe the least-capable type in TypeScript. This is useful for APIs that want to signal “this can be any value, so you must perform some type of checking before you use it”. This forces users to safely introspect returned values.
TypeScript 3.0 introduces a new type called unknown
that does exactly that. Much like any
, any value is assignable to unknown
; however, unlike any
, you cannot access any properties on values with the type unknown
, nor can you call/construct them. Furthermore, values of type unknown
can only be assigned to unknown
or any
.
As an example, swapping the above example to use unknown
instead of any
forces turns all usages of foo
into an error:
let foo: unknown = 10;
// Since `foo` has type `unknown`, TypeScript
// errors on each of these usages.
foo.x.prop;
foo.y.prop;
foo.z.prop;
foo();
new foo();
upperCase(foo);
foo `hello world!`;
function upperCase(x: string) {
return x.toUpperCase();
}
Instead, we’re now forced to either perform checking, or use a type assertion to convince the type-system that we know better.
let foo: unknown = 10;
function hasXYZ(obj: any): obj is { x: any, y: any, z: any } {
return !!obj &&
typeof obj === "object" &&
"x" in obj &&
"y" in obj &&
"z" in obj;
}
// Using a user-defined type guard...
if (hasXYZ(foo)) {
// ...we're allowed to access certain properties again.
foo.x.prop;
foo.y.prop;
foo.z.prop;
}
// We can also just convince TypeScript we know what we're doing
// by using a type assertion.
upperCase(foo as string);
function upperCase(x: string) {
return x.toUpperCase();
}
Support for defaultProps
in JSX
Default initializers are a handy feature in modern TypeScript/JavaScript. They give us a useful syntax to let callers use functions more easily by not requiring certain arguments, while letting function authors ensure that their values are always defined in a clean way.
function loudlyGreet(name = "world") {
// Thanks to the default initializer, `name` will always have type `string` internally.
// We don't have to check for `undefined` here.
console.log("HELLO", name.toUpperCase());
}
// Externally, `name` is optional, and we can potentially pass `undefined` or omit it entirely.
loudlyGreet();
loudlyGreet(undefined);
In React, a similar concept exists for components and their props
. When creating a new element React looks up a property called defaultProps
to fill in any props
that were omitted.
// Some non-TypeScript JSX file
import * as React from "react";
import * as ReactDOM from "react-dom";
export class Greet extends React.Component {
render() {
const { name } = this.props;
return <div>Hello ${name.toUpperCase()}!</div>;
}
static defaultProps = {
name: "world",
};
}
// Notice no `name` attribute was specified!
// vvvvvvvvv
const result = ReactDOM.renderToString(<Greet />);
console.log(result);
Notice that in <Greet />
, name
didn’t have to be specified. When a Greet
element is created, name
will be initialized with "world"
and this code will print <div>Hello world!</div>
.
Unfortunately, TypeScript didn’t understand that defaultProps
had any bearing on JSX invocations. Instead, users would often have to declare properties optional and use non-null assertions inside of render
:
export interface Props { name?: string }
export class Greet extends React.Component<Props> {
render() {
const { name } = this.props;
// Notice the `!` ------v
return <div>Hello ${name!.toUpperCase()}!</div>;
}
static defaultProps = { name: "world"}
}
Or they’d use some hacky type-assertions to fix up the type of the component before exporting it.
That’s why TypeScript 3.0, the language supports a new type alias in the JSX
namespace called LibraryManagedAttributes
. Despite the long name, this is just a helper type that tells TypeScript what attributes a JSX tag accepts. The short story is that using this general type, we can model React’s specific behavior for things like defaultProps
and, to some extent, propTypes
.
export interface Props {
name: string
}
export class Greet extends React.Component<Props> {
render() {
const { name } = this.props;
return <div>Hello ${name.toUpperCase()}!</div>;
}
static defaultProps = { name: "world"}
}
// Type-checks! No type assertions needed!
let el = <Greet />
Keep in mind that there are some limitations. For defaultProps
that explicitly specify their type as something like Partial<Props>
, or stateless function components (SFCs) whose defaultProps
are declared with Partial<Props>
, will make all props optional. As a workaround, you can omit the type annotation entirely for defaultProps
on a class component (like we did above), or use ES2015 default initializers for SFCs:
function Greet({ name = "world" }: Props) {
return <div>Hello ${name.toUpperCase()}!</div>;
}
Breaking changes
You can always keep an eye on upcoming breaking changes in the language as well as in our API.
We expect TypeScript 3.0 to have very few impactful breaking changes. Language changes should be minimally disruptive, and most breaks in our APIs are oriented around removing already-deprecated functions.
unknown
is a reserved type name
Since unknown
is a new built-in type, it can no longer be used in type declarations like interfaces, type aliases, or classes.
API breaking changes
- The deprecated internal method
LanguageService#getSourceFile
has been removed, as it has been deprecated for two years. See #24540. - The deprecated function
TypeChecker#getSymbolDisplayBuilder
and associated interfaces have been removed. See #25331. The emitter and node builder should be used instead. - The deprecated functions
escapeIdentifier
andunescapeIdentifier
have been removed. Due to changing how the identifier name API worked in general, they have been identity functions for a few releases, so if you need your code to behave the same way, simply removing the calls should be sufficient. Alternatively, the typesafeescapeLeadingUnderscores
andunescapeLeadingUnderscores
should be used if the types indicate they are required (as they are used to convert to or from branded__String
andstring
types). - The
TypeChecker#getSuggestionForNonexistentProperty
,TypeChecker#getSuggestionForNonexistentSymbol
, andTypeChecker#getSuggestionForNonexistentModule
methods have been made internal, and are no longer part of our public API. See #25520.
What’s next?
Since we always keep our Roadmap up-to-date, you can get a picture of what else is in store for TypeScript 3.0 and beyond there.
TypeScript 3.0 is meant to be a foundational release for project references and other powerful type system constructs. We expect it to be available at the end of the month, so until then we’ll be polishing off the release so that you can have the smoothest experience possible. In the meantime, we would love and appreciate any feedback from this RC so we can bring you that experience. Everything we do in TypeScript is driven by our users’ needs, so any contribution you can make by just trying out this release candidate would go a long way.
So feel free to drop us a line on GitHub if you run into any problems, and let others know how you feel about this RC on Twitter and in the comments below!
Happy hacking!
0 comments