Skip to content

Negated Types - #64375

Open
Wesley Wigham (weswigham) wants to merge 11 commits into
mainfrom
weswigham/negated-types
Open

Wesley Wigham (weswigham) wants to merge 11 commits into
mainfrom
weswigham/negated-types

Conversation

@weswigham

Copy link
Copy Markdown
Member

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 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

not T

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 is T
  • not (A | B | C | ...) is not A & not B & not C & not ...
  • not (A & B & C & ...) is not A | not B | not C | not ...
  • not unknown is never
  • not never is unknown
  • not any is any (since any is the NaN of types and behaves as both the bottom and top)
  • T | not T is unknown, T & not T is never

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 type not T if T is related to S.
    This follows from the set membership inversion that a negation implies - if normally a type S and a type T would be related if S is a subset of T, when we take the complements of those sets, not S and not T, those sets share an inverse relationship to the originals.
  • A type S is related to a negated type not T if the intersection of S and T 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 that S must lie within not T.
  • A negated type not S is not related to a type T.
    A negated type describes a set of values that reaches from unknown to its bound, while a normal type describes values from its bound to never - 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 in T, then it is in not 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<any>)>(value: T): void;
declare function readFileAsync(): Promise<string>;
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<T extends not null>(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<T extends not undefined>(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

Copilot AI balanced review requested due to automatic review settings September 21, 2026 17:44
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 21, 2026
@typescript-automation typescript-automation Bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 3 Medium severity · 1 Low severity

Open (5)
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,
Comment on lines +219 to +223
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);
z; // {} & Cat
break;
default:
z; // {} & not Cat
@weswigham

Copy link
Copy Markdown
Member Author

TypeScript Bot (@typescript-bot) test top1000

@typescript-automation

typescript-automation Bot commented Sep 21, 2026

Copy link
Copy Markdown

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top1000 ✅ Started 👀 Results

@typescript-automation

Copy link
Copy Markdown

Wesley Wigham (@weswigham) Here are the results of running the top 1000 repos with tsc comparing main and refs/pull/64375/merge:

Something interesting changed - please have a look.

Details

alibaba/hooks

2 of 4 projects failed to build with the old tsc and were ignored

packages/hooks/tsconfig.json

packages/hooks/tsconfig.pro.json

Companion-Inc/feynman

1 of 4 projects failed to build with the old tsc and were ignored

tsconfig.json

workbench-web/tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "application/json" & not "application/x-ndjson" & not "application/pdf"' and '"application/json"' have no overlap.

diegosouzapw/OmniRoute

4 of 10 projects failed to build with the old tsc and were ignored

tsconfig.typecheck-core.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "=" & not ":" & not "," & not ";" & not "." & not ">" & not "|" & not "'" & not "\"" & not "`"' and '":"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "localhost" & not "0.0.0.0" & not "::" & not "127.0.0.1" & not "::1"' and '"::1"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types '"nvidia" | (string & not "codex" & not "antigravity")' and '"antigravity"' have no overlap.

open-sse/tsconfig.json

electron-userland/electron-builder

9 of 12 projects failed to build with the old tsc and were ignored

test/tsconfig.json

ether/etherpad

1 of 4 projects failed to build with the old tsc and were ignored

src/tsconfig.json

facebook/lexical

17 of 34 projects failed to build with the old tsc and were ignored

tsconfig.build.json

dev-examples/shadow-dom-web-component/tsconfig.json

dev-examples/shadow-dom/tsconfig.json

dev-examples/mdast-editor/tsconfig.json

dev-examples/hmr/tsconfig.json

dev-examples/dom-import/tsconfig.json

getagentseal/codeburn

3 of 5 projects failed to build with the old tsc and were ignored

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'number & not 34' and '34' have no overlap.

homebridge/homebridge

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types '"closureControl" | "colorControl" | "levelControl" | "mediaPlayback" | "thermostat" | (string & not "rvcCleanMode" & not "serviceArea" & not "powerSource")' and '"serviceArea"' have no overlap.

tsconfig.typecheck.json

  • error TS2367: This comparison appears to be unintentional because the types '"closureControl" | "colorControl" | "levelControl" | "mediaPlayback" | "thermostat" | (string & not "rvcCleanMode" & not "serviceArea" & not "powerSource")' and '"serviceArea"' have no overlap.

krillinai/OpenCreator

4 of 11 projects failed to build with the old tsc and were ignored

apps/daemon/tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not ".jpg" & not ".jpeg" & not ".png" & not ".webp" & not ".srt" & not ".md" & not ".mp4" & not ".webm" & not ".mp3" & not ".wav" & not ".m4a"' and '".md"' have no overlap.

lidge-jun/opencodex

1 of 5 projects failed to build with the old tsc and were ignored

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types '(string & not "translation_buffer_limit") | undefined' and '"translation_buffer_limit"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "--version" & not "-v" & not "version" & not "help" & not "--help" & not "-h"' and '"help"' have no overlap.

tests/tsconfig.doctor-service-memory-contract.json

  • error TS2367: This comparison appears to be unintentional because the types '(string & not "translation_buffer_limit") | undefined' and '"translation_buffer_limit"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "--version" & not "-v" & not "version" & not "help" & not "--help" & not "-h"' and '"help"' have no overlap.

MemTensor/MemOS

5 of 9 projects failed to build with the old tsc and were ignored

apps/memos-local-plugin/tsconfig.build.json

apps/memos-local-plugin/tsconfig.json

microsoft/fast

7 of 13 projects failed to build with the old tsc and were ignored

packages/fast-element/tsconfig.api-extractor.json

packages/fast-element/tsconfig.json

packages/fast-element/test/tsconfig.json

microsoft/vscode

36 of 119 projects failed to build with the old tsc and were ignored

extensions/terminal-suggest/tsconfig.json

Narcooo/inkos

3 of 5 projects failed to build with the old tsc and were ignored

packages/core/tsconfig.json

nexu-io/open-design

23 of 52 projects failed to build with the old tsc and were ignored

apps/daemon/tsconfig.json

apps/daemon/tsconfig.tests.json

stablyai/orca

14 of 24 projects failed to build with the old tsc and were ignored

config/tsconfig.tc.web.json

teableio/teable

144 of 147 projects failed to build with the old tsc and were ignored

packages/sdk/tsconfig.json

vercel-labs/deepsec

2 of 6 projects failed to build with the old tsc and were ignored

packages/scanner/tsconfig.json

packages/processor/tsconfig.json

packages/deepsec/tsconfig.json

VSCodeVim/Vim

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "c" & not "C" & not "x" & not "X" & not "o"' and '"o"' have no overlap.

Yeachan-Heo/oh-my-claudecode

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "?" & not " "' and '"?"' have no overlap.

Yeachan-Heo/oh-my-codex

tsconfig.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "help" & not "--help" & not "-h" & not "create" & not "create-goals" & not "status" & not "add-goal" & not "steer" & not "record-review-blockers" & not "complete" & not "complete-goals" & not "next" & not "start-next"' and '"steer"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "\u001B[0m"' and '"\u001B[0m"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "node_modules/.bin/omx" & not "dist/cli/omx.js"' and '"dist/cli/omx.js"' have no overlap.

tsconfig.no-unused.json

  • error TS2367: This comparison appears to be unintentional because the types 'string & not "help" & not "--help" & not "-h" & not "create" & not "create-goals" & not "status" & not "add-goal" & not "steer" & not "record-review-blockers" & not "complete" & not "complete-goals" & not "next" & not "start-next"' and '"steer"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "\u001B[0m"' and '"\u001B[0m"' have no overlap.
  • error TS2367: This comparison appears to be unintentional because the types 'string & not "node_modules/.bin/omx" & not "dist/cli/omx.js"' and '"dist/cli/omx.js"' have no overlap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Conditional type doesn't narrow primitive types

2 participants