Negated Types - #63926
Open
Wesley Wigham (weswigham) wants to merge 1 commit into
Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Adds negated types, their syntax, type relations, inference, normalization, and control-flow narrowing.
Changes:
- Adds
not Tparsing, AST/API encoding, and declaration emit. - Implements negated-type construction, inference, assignability, and CFA.
- Adds extensive compiler tests and updated baselines.
Several regressions remain, including incorrect diagnostics, lost literal narrowing, leaked CFA types, and the still-failing #26240 case.
Reviewed changes
Copilot reviewed 255 out of 260 changed files in this pull request and generated 6 comments.
Show a summary per file
| File group | Description |
|---|---|
tsc/internal/{scanner,parser,ast,api/encoder} |
Adds not syntax and AST serialization. |
tsc/internal/checker/* |
Implements negated types, relations, inference, normalization, and narrowing. |
packages/typescript/src/{ast,api,enums}/* |
Exposes generated AST and flag changes. |
tools/scripts/tsc/ast.json, Herebyfile.mjs |
Updates code generation definitions. |
tsc/testdata/tests/cases/conformance/types/negated/* |
Adds negated-type conformance coverage. |
tsc/testdata/baselines/reference/{compiler,conformance}/* |
Updates expected diagnostics, types, symbols, emit, and declarations. |
Files not reviewed (5)
- tsc/internal/api/encoder/decoder_generated.go: Generated file
- tsc/internal/api/encoder/encoder_generated.go: Generated file
- tsc/internal/ast/ast_generated.go: Generated file
- tsc/internal/ast/kind_generated.go: Generated file
- tsc/internal/ast/kind_stringer_generated.go: Generated file
Comment on lines
+25671
to
+25675
| // Fresh negated types introduced by control flow narrowing are widened away (dropped) when a | ||
| // narrowed value escapes into a location that does not itself want a negation, so that 'not X' | ||
| // does not leak into an inferred declaration. When the contextual type does mention a negation | ||
| // (e.g. a 'not string' parameter or property), the fresh negation is preserved. | ||
| return c.getRegularTypeOfLiteralType(c.removeOrRegularizeNegatedTypes(t, containsFreshNegatedType(t) && !containsNegatedType(contextualType))) |
Comment on lines
+636
to
+637
| if introduceNegation && !doubleEquals { | ||
| return c.introduceNegationIntoNarrowedType(filtered, valueType) |
|
|
||
| type OnlyNumber<T extends number> = T; | ||
| type ToNumber<T extends number | string> = | ||
| T extends string ? undefined : OnlyNumber<T>; |
| @@ -0,0 +1,27 @@ | |||
| arrayDestructuringInSwitch1.ts(19,12): error TS2367: This comparison appears to be unintentional because the types '("false" | "true") & not any[]' and 'string' have no overlap. | |||
Comment on lines
+223
to
+227
| for _, nonNegatedType := range nonNegatedSet { | ||
| if c.isTypeSubtypeOf(nonNegatedType, negatedBounds) { | ||
| return true | ||
| } | ||
| } |
Comment on lines
+12
to
+13
| // not (A & B) stays as a single negation of the intersection. | ||
| type NotAandB = not (A & B); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR 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
Further work:
getNegatedType