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).
not not T
isT
not (A | B | C | ...)
isnot A & not B & not C & not ...
not (A & B & C & ...)
isnot A | not B | not C | not ...
not unknown
isnever
not never
isunknown
not any
isany
(sinceany
is theNaN
of types and behaves as both the bottom and top)
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.
- A negated type
not S
is related to a negated typenot T
if T is related to S.
This follows from the set membership inversion that a negation implies - if normally a typeS
and a typeT
would be related ifS
is a subset ofT
, when we take the complements of those sets,not S
andnot T
, those sets share an inverse relationship to the originals. - A type
S
is related to a negated typenot T
if the intersection ofS
andT
is empty
We want to check if for all values in S, none of those values are also in T (since if they are, S is not in the negation of T). The intersection of S and T, when simplified and evaluated, is exactly the description of the common domain of the two. If this domain is empty (never
), then we can conclude that there is no overlap between the two and thatS
must lie withinnot T
. - A negated type
not S
is not related to a typeT
.
A negated type describes a set of values that reaches fromunknown
to its bound, while a normal type describes values from its bound tonever
- it's impossible for a negated type to satisfy a normal type
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.
- A fresh object type S is related to a negated type
not T
if S is not related to T.
Since S is a singleton type, we can assume that so long as it's type is not inT
, then it is innot T
.
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).