Today we’re happy to announce the availability of our release candidate (RC) of TypeScript 3.5. Our hope is to collect feedback and early issues to ensure our final release is simple to pick up and use right away.
To get started using the RC, you can get it 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 2019/2017
- Following directions for Visual Studio Code and Sublime Text.
Let’s explore what’s new in 3.5!
- Speed improvements
- The
Omit
helper type - Improved excess property checks in union types
- The
--allowUmdGlobalAccess
flag - Smarter union type checking
- Higher order type inference from generic constructors
- Breaking changes
Speed improvements
TypeScript 3.5 introduces several optimizations around type-checking and incremental builds.
Type-checking speed-ups
Much of the expressivity of our type system comes with a cost – any more work that we expect the compiler to do translates to longer compile times. Unfortunately, as part of a bug fix in TypeScript 3.4 we accidentally introduced a regression that could lead to an explosion in how much work the type-checker did, and in turn, type-checking time. The most-impacted set of users were those using the styled-components library. This regression was serious not just because it led to much higher build times for TypeScript code, but because editor operations for both TypeScript and JavaScript users became unbearably slow.
Over this past release, we focused heavily on optimizing certain code paths and stripping down certain functionality to the point where TypeScript 3.5 is actually faster than TypeScript 3.3 for many incremental checks. Not only have compile times fallen compared to 3.4, but code completion and any other editor operations should be much snappier too.
If you haven’t upgraded to TypeScript 3.4 due to these regressions, we would value your feedback to see whether TypeScript 3.5 addresses your performance concerns!
--incremental
improvements
TypeScript 3.4 introduced a new --incremental
compiler option. This option saves a bunch of information to a .tsbuildinfo
file that can be used to speed up subsequent calls to tsc
.
TypeScript 3.5 includes several optimizations to caching how the state of the world was calculated – compiler settings, why files were looked up, where files were found, etc. In scenarios involving hundreds of projects using TypeScript’s project references in --build
mode, we’ve found that the amount of time rebuilding can be reduced by as much as 68% compared to TypeScript 3.4!
For more details, you can see the pull requests to
The Omit
helper type
Much of the time, we want to create an object that omits certain properties. It turns out that we can express types like that using TypeScript’s built-in Pick
and Exclude
helpers. For example, if we wanted to define a Person
that has no location
property, we could write the following:
type Person = { name: string; age: number; location: string; }; type RemainingKeys = Exclude<keyof Person, "location">; type QuantumPerson = Pick<Person, RemainingKeys>; // equivalent to type QuantumPerson = { name: string; age: number; };
Here we “subtracted” "location"
from the set of properties of Person
using the Exclude
helper type. We then picked them right off of Person
using the Pick
helper type.
It turns out this type of operation comes up frequently enough that users will write a helper type to do exactly this:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Instead of making everyone define their own version of Omit
, TypeScript 3.5 will include its own in lib.d.ts
which can be used anywhere. The compiler itself will use this Omit
type to express types created through object rest destructuring declarations on generics.
For more details, see the pull request on GitHub to add Omit
, as well as the change to use Omit
for object rest.
Improved excess property checks in union types
TypeScript has a feature called excess property checking in object literals. This feature is meant to detect typos for when a type isn’t expecting a specific property.
type Style = { alignment: string, color?: string }; const s: Style = { alignment: "center", colour: "grey" // ^^^^^^ error! };
In TypeScript 3.4 and earlier, certain excess properties were allowed in situations where they really shouldn’t have been. For instance, TypeScript 3.4 permitted the incorrect name
property in the object literal even though its types don’t match between Point
and Label
.
type Point = { x: number; y: number; }; type Label = { name: string; }; const thing: Point | Label = { x: 0, y: 0, name: true // uh-oh! };
Previously, a non-disciminated union wouldn’t have any excess property checking done on its members, and as a result, the incorrectly typed name
property slipped by.
In TypeScript 3.5, the type-checker at least verifies that all the provided properties belong to some union member and have the appropriate type, meaning that the sample above correctly issues an error.
Note that partial overlap is still permitted as long as the property types are valid.
const pl: Point | Label = { x: 0, y: 0, name: "origin" // okay };
The --allowUmdGlobalAccess
flag
In TypeScript 3.5, you can now reference UMD global declarations like
export as namespace foo;
from anywhere – even modules – using the new --allowUmdGlobalAccess
flag.
This feature might require some background if you’re not familiar with UMD globals in TypeScript. A while back, JavaScript libraries were often published as global variables with properties tacked on – you sort of hoped that nobody picked a library name that was identical to yours. Over time, authors of modern JavaScript libraries started publishing using module systems to prevent some of these issues. While module systems alleviated certain classes of issues, they did leave users who were used to using global variables out in the rain.
As a work-around, many libraries are authored in a way that define a global object if a module loader isn’t available at runtime. This is typically leveraged when users target a module format called “UMD”, and as such, TypeScript has a way to describe this pattern which we’ve called “UMD global namespaces”:
export as namespace preact;
Whenever you’re in a script file (a non-module file), you’ll be able to access one of these UMD globals.
So what’s the problem? Well, not all libraries conditionally set their global declarations. Some just always create a global in addition to registering with the module system. We decided to err on the more conservative side, and many of us felt that if a library could be imported, that was probably the the intent of the author.
In reality, we received a lot of feedback that users were writing modules where some libraries were consumed as globals, and others were consumed through imports. So in the interest of making those users’ lives easier, we’ve introduced the allowUmdGlobalAccess
flag in TypeScript 3.5.
For more details, see the pull request on GitHub.
Smarter union type checking
When checking against union types, TypeScript typically compares each constituent type in isolation. For example, take the following code:
type S = { done: boolean, value: number } type T = | { done: false, value: number } | { done: true, value: number }; declare let source: S; declare let target: T; target = source;
Assigning source
to target
involves checking whether the type of source
is assignable to target
. That in turn means that TypeScript needs to check whether S
:
{ done: boolean, value: number }
is assignable to T
:
{ done: false, value: number } | { done: true, value: number }
Prior to TypeScript 3.5, the check in this specific example would fail, because S
isn’t assignable to { done: false, value: number }
nor { done: true, value: number }
. Why? Because the done
property in S
isn’t specific enough – it’s boolean
whereas each constituent of T
has a done
property that’s specifically true
or false
. That’s what we meant by each constituent type being checked in isolation: TypeScript doesn’t just union each property together and see if S
is assignable to that. If it did, some bad code could get through like the following:
interface Foo { kind: "foo"; value: string; } interface Bar { kind: "bar"; value: number; } function doSomething(x: Foo | Bar) { if (x.kind === "foo") { x.value.toLowerCase(); } } // uh-oh - luckily TypeScript errors here! doSomething({ kind: "foo", value: 123, });
So clearly this behavior is good for some set of cases. Was TypeScript being helpful in the original example though? Not really. If you figure out the precise type of any possible value of S
, you can actually see that it matches the types in T
exactly.
That’s why in TypeScript 3.5, when assigning to types with discriminant properties like in T
, the language actually will go further and decompose types like S
into a union of every possible inhabitant type. In this case, since boolean
is a union of true
and false
, S
will be viewed as a union of { done: false, value: number }
and { done: true, value: number }
.
For more details, you can see the original pull request on GitHub.
Higher order type inference from generic constructors
In TypeScript 3.4, we improved inference for when generic functions that return functions like so:
function compose<T, U, V>( f: (x: T) => U, g: (y: U) => V): (x: T) => V { return x => g(f(x)) }
took other generic functions as arguments, like so:
function arrayify<T>(x: T): T[] { return [x]; } type Box<U> = { value: U } function boxify<U>(y: U): Box<U> { return { value: y }; } let newFn = compose(arrayify, boxify);
Instead of a relatively useless type like (x: {}) => Box<{}[]>
, which older versions of the language would infer, TypeScript 3.4’s inference allows newFn
to be generic. Its new type is <T>(x: T) => Box<T[]>
.
TypeScript 3.5 generalizes this behavior to work on constructor functions as well.
class Box<T> { kind: "box"; value: T; constructor(value: T) { this.value = value; } } class Bag<U> { kind: "bag"; value: U; constructor(value: U) { this.value = value; } } function composeCtor<T, U, V>( F: new (x: T) => U, G: new (y: U) => V): (x: T) => V { return x => new G(new F(x)) } let f = composeCtor(Box, Bag); // has type '<T>(x: T) => Bag<Box<T>>' let a = f(1024); // has type 'Bag<Box<number>>'
In addition to compositional patterns like the above, this new inference on generic constructors means that functions that operate on class components in certain UI libraries like React can more correctly operate on generic class components.
type ComponentClass<P> = new (props: P) => Component<P>; declare class Component<P> { props: P; constructor(props: P); } declare function myHoc<P>(C: ComponentClass<P>): ComponentClass<P>; type NestedProps<T> = { foo: number, stuff: T }; declare class GenericComponent<T> extends Component<NestedProps<T>> { } // type is 'new <T>(props: NestedProps<T>) => Component<NestedProps<T>>' const GenericComponent2 = myHoc(GenericComponent);
To learn more, check out the original pull request on GitHub.
Breaking changes
Generic type parameters are implicitly constrained to unknown
In TypeScript 3.5, generic type parameters without an explicit constraint are now implicitly constrained to unknown
, whereas previously the implicit constraint of type parameters was the empty object type {}
.
In practice, {}
and unknown
are pretty similar, but there are a few key differences:
{}
can be indexed with a string (k["foo"]
), though this is an implicitany
error under--noImplicitAny
.{}
is assumed to not benull
orundefined
, whereasunknown
is possibly one of those values.{}
is assignable toobject
, butunknown
is not.
The decision to switch to unknown
is rooted that it is more correct for unconstrained generics – there’s no telling how a generic type will be instantiated.
On the caller side, this typically means that assignment to object
will fail, and methods on Object
like toString
, toLocaleString
, valueOf
, hasOwnProperty
, isPrototypeOf
, and propertyIsEnumerable
will no longer be available.
function foo<T>(x: T): [T, string] { return [x, x.toString()] // ~~~~~~~~ error! Property 'toString' does not exist on type 'T'. }
As a workaround, you can add an explicit constraint of {}
to a type parameter to get the old behavior.
// vvvvvvvvvv function foo<T extends {}>(x: T): [T, string] { return [x, x.toString()] }
From the caller side, failed inferences for generic type arguments will result in unknown
instead of {}
.
function parse<T>(x: string): T { return JSON.parse(x); } // k has type 'unknown' - previously, it was '{}'. const k = parse("...");
As a workaround, you can provide an explicit type argument:
// 'k' now has type '{}' const k = parse<{}>("...");
{ [k: string]: unknown }
is no longer a wildcard assignment target
The index signature { [s: string]: any }
in TypeScript behaves specially: it’s a valid assignment target for any object type. This is a special rule, since types with index signatures don’t normally produce this behavior.
Since its introduction, the type unknown
in an index signature behaved the same way:
let dict: { [s: string]: unknown }; // Was okay dict = () => {};
In general this rule makes sense; the implied constraint of “all its properties are some subtype of unknown
” is trivially true of any object type. However, in TypeScript 3.5, this special rule is removed for { [s: string]: unknown }
.
This was a necessary change because of the change from {}
to unknown
when generic inference has no candidates. Consider this code:
declare function someFunc(): void; declare function fn<T>(arg: { [k: string]: T }): void; fn(someFunc);
In TypeScript 3.4, the following sequence occurred:
- No candidates were found for
T
T
is selected to be{}
someFunc
isn’t assignable toarg
because there are no special rules allowing arbitrary assignment to{ [k: string]: {} }
- The call is correctly rejected
Due to changes around unconstrained type parameters falling back to unknown
(see above), arg
would have had the type { [k: string]: unknown }
, which anything is assignable to, so the call would have incorrectly been allowed. That’s why TypeScript 3.5 removes the specialized assignability rule to permit assignment to { [k: string]: unknown }
.
Note that fresh object literals are still exempt from this check.
const obj = { m: 10 }; // okay const dict: { [s: string]: unknown } = obj;
Depending on the intended behavior of { [s: string]: unknown }
, several alternatives are available:
{ [s: string]: any }
{ [s: string]: {} }
object
unknown
any
We recommend sketching out your desired use cases and seeing which one is the best option for your particular use case.
Improved excess property checks in union types
As mentioned above, TypeScript 3.5 is stricter about excess property checks on constituents of union types.
We have not witnessed examples where this checking hasn’t caught legitimate issues, but in a pinch, any of the workarounds to disable excess property checking will apply:
- Add a type assertion onto the object (e.g.
{ myProp: SomeType } as ExpectedType
) - Add an index signature to the expected type to signal that unspecified properties are expected (e.g.
interface ExpectedType { myProp: SomeType; [prop: string]: unknown }
)
Fixes to unsound writes to indexed access types
TypeScript allows you to represent the operation of accessing a property of an object via the name of that property:
type A = { s: string; n: number; }; function read<K extends keyof A>(arg: A, key: K): A[K] { return arg[key]; } const a: A = { s: "", n: 0 }; const x = read(a, "s"); // x: string
While commonly used for reading values from an object, you can also use this for writes:
function write<K extends keyof A>(arg: A, key: K, value: A[K]): void { arg[key] = value; }
In TypeScript 3.4, the logic used to validate a write was much too permissive:
function write<K extends keyof A>(arg: A, key: K, value: A[K]): void { // ??? arg[key] = "hello, world"; } // Breaks the object by putting a string where a number should be write(a, "n");
In TypeScript 3.5, this logic is fixed and the above sample correctly issues an error.
Most instances of this error represent potential errors in the relevant code. If you are convinced that you are not dealing with an error, you can use a type assertion instead.
lib.d.ts
includes the Omit
helper type
TypeScript 3.5 includes a new Omit
helper type. As a result, any global declarations of Omit
included in your project will result in the following error message:
Duplicate identifier 'Omit'.
Two workarounds may be used here:
- Delete the duplicate declaration and use the one provided in
lib.d.ts
. - Export the existing declaration from a module file or a namespace to avoid a global collision. Existing usages can use an
import
or explicit reference to your project’s oldOmit
type.
Object.keys
rejects primitives in ES5
In ECMAScript 5 environments, Object.keys
throws an exception if passed any non-object
argument:
// Throws if run in an ES5 runtime Object.keys(10);
In ECMAScript 2015, Object.keys
returns []
if its argument is a primitive:
// [] in ES6 runtime Object.keys(10);
This is a potential source of error that wasn’t previously identified. In TypeScript 3.5, if target
(or equivalently lib
) is ES5
, calls to Object.keys
must pass a valid object
.
In general, errors here represent possible exceptions in your application and should be treated as such. If you happen to know through other means that a value is an object
, a type assertion is appropriate:
function fn(arg: object | number, isArgActuallyObject: boolean) { if (isArgActuallyObject) { const k = Object.keys(arg as object); } }
Note that this change interacts with the change in generic inference from {}
to unknown
, because {}
is a valid object
whereas unknown
isn’t:
declare function fn<T>(): T; // Was okay in TypeScript 3.4, errors in 3.5 under --target ES5 Object.keys(fn());
What’s next?
The final release of TypeScript 3.5 should be coming out at the end of the month. We encourage you to give the RC a try so we can ensure TypeScript 3.5 provides the ideal coding experience.
Happy hacking!
- Daniel Rosenwasser and the TypeScript Team
The post Announcing TypeScript 3.5 RC appeared first on TypeScript.