Skip to content

Instantly share code, notes, and snippets.

@RyanCavanaugh
Last active May 15, 2019 22:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RyanCavanaugh/8e87a9d1fd0bb398326fddf7a54b4b9c to your computer and use it in GitHub Desktop.
Save RyanCavanaugh/8e87a9d1fd0bb398326fddf7a54b4b9c to your computer and use it in GitHub Desktop.
Breaking Changes 3.5

Object.keys rejects primitives in ES5

Background

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 6, Object.keys returns [] if its argument is a primitive:

// [] in ES6 runtime
Object.keys(10);

Rationale and Change

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.

Workarounds

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 OK in TypeScript 3.4, errors in 3.5 under --target ES5
Object.keys(fn());

Improved Checking of Excess Properties in Union Assignability

Background

TypeScript has a feature called excess property checking that detects typos in common situations:

type Style = {
	alignment: string,
	color?: string
};
const s: Style = {
	alignment: "center",
	colour: "grey"
};

This checking is very specific to certain patterns because structural subtyping is common:

function printPoint(pt: { x: number, y: number }) {
	console.log(pt.x, ', ', pt.y);
}
const Origin = {
	x: 0,
	y: 0,
	name: "origin"
};
// OK - 'name' is not an error here
printPoint(Origin);

Rationale and Change

In TypeScript 3.4 and earlier, certain excess properties were allowed in situations where they really shouldn't have been.

Consider this code:

type Point = {
	x: number;
	y: number;
};
type Label = {
	name: string;
};

const pl: Point | Label = {
	x: 0,
	y: 0,
	name: true // <- danger!
};

Excess property checking was previously only capable of detecting properties which weren't present in any member of a target union type.

In TypeScript 3.5, these excess properties are now correctly detected, and the sample above correctly issues an error.

Note that it's still legal to be assignable to multiple parts of a union:

const pl: Point | Label = {
	x: 0,
	y: 0,
	name: "origin" // OK
};

Workarounds

Every instance of this has so far represented a legitimate bug in the codebase it was found in. We don't have any recommended workarounds other than to simply fix those bugs.

Zero-Candidate Generic Inference produces unknown instead of {}

Background

During a generic function call, TypeScript relates the function arguments to its parameters to determine the type parameters' types:

type Box<T> = { contents: T };
function box<T>(arg: T): Box<T> {
    return {
        contents: arg;
    }
}

// box's 'T' parameter inferred to be 'string'
const p = box("hello");

Each possible type for a type parameter is called a candidate, and TypeScript selects the "best" candidate according to a set of criteria. However, sometimes there are no candidates:

type Box<T> = { contents: T };
function emptyBox<T>(): Box<T | undefined> {
    return {
        contents: undefined;
    }
}

// T: ??
const p = emptyBox();

In prior versions of TypeScript, T would be {}, or the constraint type if one was present.

Rationale and Change

At the time generics were added to the language, {} was the most accurate choice available, but unknown is a safer default. In TypeScript 3.5, when inference finds no candidates for a type parameter, the type unknown will be used instead if there is no constraint type.

unknown is a much more accurate representation of what will happen at runtime. Critically, if a generic function f<T> is not given a value of type T, it has no ability to produce a value of type T, and the caller should not assume anything about values in the return position typed as T.

Prevalence

Having zero inference candidates in a function call should be rare. The most common place where this does occur is in code that uses generics to appear more typesafe than it is:

function parse<T>(x: string): T {
    return JSON.parse(x);
}

// Generic call, must be safe??
const k = parse<string[]>("[0]");

This pattern is misleading and should be avoided! Because an unsafe operation is occurring, it's more appropriate to use a type assertion:

function parse(x: string): unknown {
    return JSON.parse(x);
}

// Unsafety is more obvious
const k = parse("[0]") as string[];

When these functions are called without type parameters, unknown is now produced automatically:

function parse<T>(x: string): T {
    return JSON.parse(x);
}

// k: unknown (was {})
const k = parse("[0]");

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 implicit any error under --noImplicitAny
  • {} is assumed to not be null or undefined, whereas unknown is possibly one of those values
  • {} is assignable to object, but unknown is not

If your code depended on one of these aspects of {} types produced from generic calls, you may see new errors in 3.5.

Workarounds

Two immediate changes are available.

You can add a constraint to the function; this will cause any call to produce {} instead of unknown

// Will default to unknown
declare function fn1<T>(): T;
// Will default to {}
declare function fn2<T extends {}>(): T;

You can also specify the type argument at the call site:

// k will now be {} instead of unknown
const k = fn1<{}>();

{ [k: string]: unknown } no longer a wildcard assignment target

Background

The index signature { [s: string]: any } in TypeScript behaves specially: It is a valid assignment target for any object type. This is a special rule - the definition of index signatures would not normally produce this behavior.

Since its introduction, the type unknown in an index signature behaved the same way:

let dict: { [s: string]: unknown };
// Was OK
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.

Rationale and Change

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 to arg because there are no special rules allowing arbitrary assignment to { [k: string]: {} }
  • The call is correctly rejected

In TypeScript 3.5, prior to this breaking change, the following sequence occurred:

  • No candidates were found for T
  • T is selected to be unknown
  • someFunc is assignable to arg because of the special rule allowing arbitrary assignment to { [k: string]: unknown }
  • The call is incorrectly allowed

In TypeScript 3.5, with this change, the following sequence occurrs:

  • No candidates were found for T
  • T is selected to be unknown
  • someFunc not assignable to arg because it doesn't have a compatible index signature
  • The call is correctly rejected

Note that the existing behavior allowing the inference of an index signature from a type originating in a fresh object literal is still preserved:

const obj = { m: 10 }; 
// OK
const dict: { [s: string]: unknown } = obj;

Workarounds

Some codebases have adopted { [s: string]: unknown } to mean "an arbitrary object". Depending on the intended behavior, several alternatives are available:

  • object
  • { [s: string]: any }
  • { [s: string]: {} }

We recommend sketching out your desired use cases and seeing which one is the best option for your particular use case.

Unsound Writes to Indexed Access Types

Background

TypeScript allows you to represent the abstract 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;
}

Change and Rationale

In TypeScript 3.5, 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.6, this logic is fixed and the above sample correctly issues an error.

Workarounds

Most instances of this error represent potential errors in the relevant code.

One example we found looked like this:

type T = {
    a: string,
    x: number,
    y: number
};
function write<K extends keyof T>(obj: T, k: K) {
    // Trouble waiting
    obj[k] = 1;
}
const someObj: T = { a: "", x: 0, y: 0 };
// Note: write(someObj, "a") never occurs, so the code is technically bug-free (?)
write(someObj, "x");
write(someObj, "y");

This function can be fixed to only accept keys which map to numeric properties:

// Generic helper type that produces the keys of an object
// type which map to properties of some other specific type
type KeysOfType<TObj, TProp, K extends keyof TObj = keyof TObj> = K extends K ? TObj[K] extends TProp ? K : never : never;

function write(obj: SomeObj, k: KeysOfType<SomeObj, number>) {
    // OK
    obj[k] = 1;
}

const someObj: SomeObj = { a: "", x: 0, y: 0 };
write(someObj, "x");
write(someObj, "y");
// Correctly an error
write(someObj, "a");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment