Negated Types - #64375
Negated Types#64375Wesley Wigham (weswigham) wants to merge 11 commits into
Conversation
…t literal handling
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Core complement identities and conditional false-branch narrowing remain incorrect, and the new public type kind lacks consistent API introspection.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (5)
Apply negated constraint when checking conditional false branches · New Expose NegatedType base and type guards in both APIs · New Compare combined non-negated intersections against negated bounds · New Implement De Morgan normalization for negated intersections · New Correct expected type comment to document {} behavior · New
What changed in this PR
Adds first-class not T negated types across the compiler, control-flow analysis, declaration emit, and public APIs.
Changes:
- Implements parsing, normalization, assignability, inference, and narrowing.
- Exposes negation through synchronous and asynchronous APIs.
- Adds extensive compiler tests and regenerated baselines.
| File | Description |
|---|---|
Herebyfile.mjs |
Updates generation and smoke-test configuration. |
tools/scripts/tsc/ast.json |
Adds NotKeyword AST metadata. |
tsc/internal/{ast,scanner,parser}/... |
Adds syntax support for not. |
tsc/internal/checker/{types,negated,checker,flow,relater,inference,nodebuilderimpl,exports}.go |
Implements negated-type semantics and API exposure. |
tsc/internal/api/... |
Adds protocol handling and AST encoding. |
packages/typescript/src/{ast,enums}/... |
Publishes generated syntax and flag definitions. |
packages/typescript/src/api/{sync,async,node}/... |
Adds client API and protocol support. |
packages/typescript/test/{sync,async}/... |
Tests API requests and batching parity. |
tsc/testdata/tests/cases/{compiler,conformance}/...negated... |
Covers normalization, inference, narrowing, emit, and regressions. |
tsc/testdata/baselines/reference/{compiler,conformance}/... |
Records expected diagnostics, types, symbols, and emit. |
tsc/testdata/fixtures/compiler/checker.ts |
Corrects declaration-extension selection. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| type OnlyNumber<T extends number> = T; | ||
| type ToNumber<T extends number | string> = | ||
| T extends string ? undefined : OnlyNumber<T>; |
| Reserved1 = 1 << 29, | ||
| Reserved2 = 1 << 30, | ||
| Reserved3 = 1 << 31, | ||
| Negated = 1 << 25, |
| for _, nonNegatedType := range nonNegatedSet { | ||
| if c.isTypeSubtypeOf(nonNegatedType, negatedBounds) { | ||
| return true | ||
| } | ||
| } |
| // not (A & B) stays as a single negation of the intersection. | ||
| type NotAandB = not (A & B); |
| z; // {} & Cat | ||
| break; | ||
| default: | ||
| z; // {} & not Cat |
|
TypeScript Bot (@typescript-bot) test top1000 |
|
Wesley Wigham (@weswigham) Here are the results of running the top 1000 repos with tsc comparing Something interesting changed - please have a look. Details
|



This PR is a clone of #63926, but as a branch on the main repo, so it can be stacked against, which itself is a port of microsoft/typescript-go#4200 which is a port of #29317 for this codebase, with further work to enable control flow creation of negations added on.
To repeat from those PRs:
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
stringcovers all values which are strings at runtime, a "notstring" 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 thetruebranch, we can just intersect theextendsclause type, however in thefalsebranch 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 typefalsebranch implies, and allowing it to stand alone as a type.Syntax
where
Tis another type. I'm open to bikeshedding this, or even shipping without syntax available, but among alternatives (!,~)notreads 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 TisTnot (A | B | C | ...)isnot A & not B & not C & not ...not (A & B & C & ...)isnot A | not B | not C | not ...not unknownisnevernot neverisunknownnot anyisany(sinceanyis theNaNof types and behaves as both the bottom and top)T | not Tisunknown,T & not TisneverAssignability 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.
not Sis related to a negated typenot Tif T is related to S.This follows from the set membership inversion that a negation implies - if normally a type
Sand a typeTwould be related ifSis a subset ofT, when we take the complements of those sets,not Sandnot T, those sets share an inverse relationship to the originals.Sis related to a negated typenot Tif the intersection ofSandTis emptyWe 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 thatSmust lie withinnot T.not Sis not related to a typeT.A negated type describes a set of values that reaches from
unknownto its bound, while a normal type describes values from its bound tonever- it's impossible for a negated type to satisfy a normal typeAssignability 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.
not Tif S is not related to T.Since S is a singleton type, we can assume that so long as it's type is not in
T, 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:
Fixes #26240.
Allows #27711 to be cleanly fixed with a lib change (example in the tests).
Ref #4183, #4196, #7648, #12215, #18280