Negated types by weswigham · Pull Request #29317 · microsoft/TypeScript (original) (raw)

Long have we spoken of them in hushed tones and referenced them in related issues, here they are:

Negated Types

Negated types, as the name may imply, are the negation of another type. Conceptually, this means that if string covers all values which are strings at runtime, a "not string" covers all values which are... not. We had hoped that conditional types would by and large subsume any use negated types would have... and they mostly do, except in many cases we need to apply the constraint implied by the conditional's check to it's result. In the true branch, we can just intersect the extends clause type, however in the false branch we've thus far been discarding the information. This means unions may not be filtered as they should (especially when conditionals nest) and information can be lost. So that ended up being the primary driver for this primitive - it's taking what a conditional type false branch implies, and allowing it to stand alone as a type.

Syntax

where T is another type. I'm open to bikeshedding this, or even shipping without syntax available, but among alternatives (!, ~) not reads pretty well.

Identities

These are little tricks we do on negated type construction to help speed things along (and give negations on algebraic types canonical forms).

Assignability Rules

Negated types, for perhaps obvious reasons, cannot be related structurally - the only sane way to relate them is in a higher-order fashion. Thus, the rules governing these relations are very important.

Assignability Addendum for Fresh Object Types

Frequently we want to consider a fresh object type as a singleton type (indeed, some examples in the refs assume this) - it corresponds to one runtime value, not the bounds on a value (meaning, as a type, both its upper and lower bounds are itself). Using this, we can add one more rule that allows fresh literal types to easily satisfy negated object types.

Examples

Examples of negated type usage can be found in the tests of this PR (there's a few hundred lines of them, and probably some more to come for good measure), but here's some of the common ones, pulled from the referenced issues:

declare function ignore<T extends not (object & Promise)>(value: T): void; declare function readFileAsync(): Promise; declare function readFileSync(): string; ignore(readFileSync()); // OK ignore(readFileAsync()); // Should error

declare function map<T, U extends not void>(values: T[], map: (value: T) => U) : U[]; // validate map callback doesn't return void

function foo() {}

map([1, 2, 3], n => n + 1); // OK map([1, 2, 3], foo); // Should error

function asValid(value: T, isValid: (value: T) => boolean) : T | null { return isValid(value) ? value : null; }

declare const x: number; declare const y: number | null; asValid(x, n => n >= 0); // OK asValid(y, n => n >= 0); // Should error

function tryAt(values: T[], index: number): T | undefined { return values[index]; }

declare const a: number[]; declare const b: (number | undefined)[]; tryAt(a, 0); // OK tryAt(b, 0); // Should error

Fixes #26240.
Allows #27711 to be cleanly fixed with a lib change (example in the tests).

Ref #4183, #4196, #7648, #12215, #18280