Repository navigation
Design Meeting Notes, 2026-08-27 #64113
Copy link
Copy link
Open
Labels
Design NotesNotes from our design meetingsNotes from our design meetings
Description
Negated Types (not T)
- If you think about a venn diagram of
AandBoverlapping whereA & Bis the overlap,not Ais all the area outside ofA,not (A & B)is the inverse color of that original overlap. - Fresh object types are closed -
- Talked about this before, have a feeling it's more relevant today.
- Like what?
-
Something like a
--noUnusedReturnedValues-
Don't want to forget
-
People use
void, but this acceptsPromises that need to beawaited. -
Can have an
ignoreto help here.function ignore(value: not PromiseLike<unknown>): void {}
-
-
`foo${string & not "reserved-key"}`Record<`on${string & not keyof EventNames}`, unknown>
-
Can also do narrowing for carve outs of infinite sets.
function divide(a: number, b: number & not 0): number { return a / b; } function doStuff(someValue: number) { if (someValue !== 0) { divide(10, x); // Okay // (come back to this below) const x = someValue; divide(10, x); // ... } else { divide(10, someValue); // Should error! } }
- Okay, but how does that work with aliases
- Currently an alias loses that information.
- That is super surprising.
- Okay, but how does that work with aliases
-
- What are the old issues that this avoids?
- We had a prototype that swapped out our fact-tracking system in control flow analysis and replaced it with negated type tracking.
- But this was computationally intensive.
- This PR introduces negations only for specific types of control flow narrowing in the false branch where we needed to track that info.
- Only requests negation when a use-site of a binding needs negation.
- Done based on the contextual type at that use-site.
- We actually have similar logic here for when we narrow generic parameters in the body of a function!
- Very similar precedent here.
- Do you want the most precise type to be inferred
- e.g. type narrowing conditionals (come back to this?)
- We also infer type predicates for you now.
- Negated types seem useful over the set of primitives, and often works well enough in the object hierarchy, but doesn't really make sense to have a provable negated object type.
- An
objectdoesn't necessarily exclude aPromiseor aFunction. - But all sorts of checks like this are all heuristics-based.
- An
- Could have a switch to check for negations?
- But is this a strictness thing?
- Feels like no - weird.
- Does this feel consistent?
- Should do this stuff everywhere.
- But if you have a switch case with 200 cases, you'll get 200
nots in the default, whether you cared or not. This PR avoids that unless you actually need the negation.
- Let's come back to use-cases.
not Promise- Control flow issues.
- With this PR, it is very nice that it's both faster and that you don't see the
nots in hover unless you need. - We do want to experiment a bit more with decomposing existing primitives here. There are some unknown unknowns here.
- Is
objectequivalent to{} & not number & not string & not boolean & not symbol? - Is
{}equivalent tonot null & not undefined? - Maybe
ExtractandExcludecan be defined in this way?- Or rather, have the compiler produce intersections of negations wherever we otherwise produce these.
- Aside: added a specific mechanism for reducing intersections in the presence of negations.
- Or rather, have the compiler produce intersections of negations wherever we otherwise produce these.
- Is
- What does negation mean in the presence of optional properties?
not { optionalProp?: Type }- Out of time!
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Design NotesNotes from our design meetingsNotes from our design meetings
Activity
DanielRosenwasser commentedon Sep 1, 2026
An optional property in
{ a?: string }is sort of likenot { a: unknown } | { a: string }. If you trust that desugaring, I guess De Morgan's law says it's{ a: unknown } & not { a: string }.In other words,
amust be present, but it better not be astring.DanielRosenwasser commentedon Sep 1, 2026
Something else that's to think about is that
{ a: string, b: number }is roughly{ a: string } & { b: number }.So I'd expect
not { a: string, b: number }to be consistent withnot { a: string } | not { b: number }?DanielRosenwasser commentedon Sep 1, 2026
We nerd-sniped Ryan Cavanaugh (@RyanCavanaugh) into talking about what the heck
not voidis.RyanCavanaugh commentedon Sep 1, 2026
Here's an AI summary of the discussion. The recording started a little bit late
Negated Types in TypeScript — Design Discussion Summary
1. The proposed type constructor and its immediate uses
The discussion opens by finishing a point about fresh object types. TypeScript normally treats object types as open: an object described as
{ x: number }may possess additional properties that are not mentioned in its static type. This makes it difficult to prove that an object is not assignable to some other structural type. Fresh object literals are different because excess-property checking gives the compiler a limited form of closed-world knowledge. When checking a fresh object literal, the compiler can sometimes conclude that the object definitely lacks the additional properties needed to satisfy another type. That information can participate in reasoning about a negated type.The conversation then moves from relationship rules to concrete uses. The proposed constructor can be understood schematically as
not T, usually appearing in an intersection:This denotes the portion of
Sthat cannot overlap withT. Unlike today'sExclude<S, T>, this is intended to be a primitive type-system operation rather than a distributive conditional over the presently visible union constituents ofS.Several practical examples motivate the feature.
Preventing accidentally ignored promises
One use is a replacement for
voidin APIs or lint-oriented helpers that intentionally discard values. Existing rules can conflict:void expression.A parameter accepting "anything except a promise" could express the intended policy directly:
The exact syntax was still conceptual, but the semantic goal was clear: ordinary return values can be ignored, while promise-like values produce an error. The same capability could improve standard-library declarations such as serialization APIs, where passing a promise is almost always a forgotten
await.Excluding
void,null, orundefinedNegation also permits direct constraints such as:
A mapping operation could require a callback that produces a meaningful value rather than
void. An API could rejectnullwhile continuing to admitundefined, or vice versa, without reconstructing the allowed universe as a union.Today, some of this is approximated through special types and special compiler behavior. A first-class negation operation would generalize the idea to values such as zero and the empty string:
These are particularly useful for functions whose runtime preconditions require a nonzero divisor, a nonempty identifier, or another value that cannot be represented as a positive union of all valid possibilities.
Negated template-literal patterns and property keys
Template-literal types considerably strengthen the case for negation compared with earlier versions of the proposal. A negated template can describe strings that do not match a pattern. Combined with index signatures or mapped types, this can describe broad key spaces with explicit exceptions—for example, all keys beginning with a prefix except one reserved key.
More generally, a key type such as:
can describe all string keys other than the keys already declared by
T. Giving those remaining keys the value typenevercreates a form of pseudo-closed object type:The precise formulation would depend on the final syntax and index-signature rules, but the important capability is exclusion from an infinite key domain. Existing template-literal relationship machinery reportedly handled much of this naturally, requiring little additional implementation.
2. Selective control-flow production of negations
The next part examines how negated types should interact with narrowing. An older implementation had replaced much of the compiler's type-fact machinery with negated types. Every failed test could add another negation to the flow type:
After a long
ifchain orswitch, dozens of such terms could accumulate. Although each negation makes the denoted value set smaller, it creates another compiler type object and more relationship work. Thus semantic refinement paradoxically increased the internal representation and computational cost at every branch.The current proposal is much more conservative. Control flow creates negated types only when all of the following roughly hold:
For example:
The parameter type tells the checker that it needs to prove
xisnumber & not 0. Control-flow analysis can then materialize that negation in the guarded branch.These flow-produced negations are treated as fresh and are removed at widening boundaries. A negation explicitly written in a declaration is durable; a negation manufactured solely to satisfy a contextual use can disappear when the value is assigned to an unconstrained local:
The initializer of
yhas no negated contextual type, so the compiler may infer an ordinarynumber. This prevents negations from leaking through arbitrary code and appearing in inferred public APIs.The design was compared to other forms of TypeScript freshness, especially unique-symbol and literal freshness. It is not object-literal freshness in the excess-property-checking sense; rather, it marks an inferred refinement that may be discarded when its motivating context no longer exists.
3. The central disagreement: pragmatism versus a coherent end state
The temporary-variable example becomes the central point of contention. One side views the information loss as an expected consequence of TypeScript's existing contextual typing and widening rules. Similar behavior already occurs when a contextually typed function expression is extracted into a local declaration, or when a narrowed primitive literal passes through a widening binding. Under this view, selectively constructing negations is consistent with existing pragmatic compromises.
The opposing concern is not that this particular inconsistency is unprecedented. It is that introducing a foundational operation without understanding its eventual role may add another permanent layer of ad hoc rules. Users will reasonably expect a harmless refactoring—introducing a temporary—to preserve a proven precondition. If a direct call accepts a value but the equivalent call through a local rejects it, many users will report the behavior as a bug.
A related example involves inferred return types:
In a maximally precise system, the inferred return type might be:
Likewise, a runtime truthiness assertion might return something approximating:
The conservative implementation can possess these facts internally but erase them at the return boundary unless a declaration or context explicitly requires them. That prevents novel types from appearing throughout existing programs, but it also gives up an important benefit: automatically inferring reusable predicates and assertion functions.
The debate therefore concerns the scope of inference more than the value of the primitive itself. TypeScript's inference is one of its defining strengths. Requiring users to write every useful negated result explicitly could make the feature feel artificially constrained. Conversely, inferring maximal refinements everywhere would produce large, unfamiliar hover types and significant compatibility churn.
No simple "always infer the most precise type" principle exists in today's language. Overloads are not inferred from implementation branches; return types often become unions; contextual predicates and generic narrowing are inferred only in selected positions. The language contains a decade of individually motivated rules that do not form a single uniform theory. Negated types expose those inconsistencies because they make previously inexpressible intermediate states representable.
4. Whether the feature requires an opt-in mode
The group repeatedly distinguishes two changes:
The first is almost entirely additive. Existing programs contain no negated annotations, so the constructor alone should have little observable effect. The second category can change inferred types and reject code that previously compiled.
One proposed direction is an opt-in mode representing the more principled world the language might have implemented had negation existed from the beginning. Such a mode could:
The concern is that another mode would further complicate TypeScript's configuration story. Existing strictness settings already combine rules with different histories and motivations. Adding another "more correct" mode risks creating multiple dialects whose exact semantics are difficult to explain.
The alternative is a pay-for-play model: negations become active only after user-written types introduce them. That minimizes compatibility risk and keeps ordinary hovers readable. Its weakness is that contextual demand becomes an invisible trigger. Two expressions that are operationally equivalent may receive different precision depending on whether a negated target type is visible at the immediate use site.
5. Conditional types and negative constraints
The conversation then turns to conditional types, which act as a form of type-level control flow:
In the true branch, TypeScript can record the positive constraint that
AextendsB. The tempting symmetric rule is to treat the false branch as ifAextendsnot B.That rule is not universally sound. Failure of
A extends Bdoes not necessarily mean every inhabitant ofAis outsideB.Amight partially overlapB, especially when unions, generic types, distributivity, and non-unit structural types are involved. Applying a straightforward Boolean complement or De Morgan transformation can therefore overstate what the failed conditional proves. The test is not always an "if and only if" partition of the type universe.This distinction matters for higher-order relationships. A negative constraint introduced in a false branch could cause another generic constraint to fail, potentially fixing apparent type holes but doing so on an unsound premise. The relationship machinery can safely use negation when it can prove that an intersection is empty:
That is stronger than merely knowing that
Ais not assignable toB.Despite this difficulty, conditional-type integration was not viewed as a reason to reject the primitive. It was treated as an important but separable design problem. Earlier implementations had experimented with false-branch negative constraints, and the amount of implementation code was not thought to be large. The challenge is identifying exactly where the inference is semantically justified.
6. Structural objects and the meaning of "not an object type"
Negation is clearest for primitive singleton values:
It becomes subtler for structural objects because TypeScript object types are generally open. A value assignable to
{ x: number }may also have every property needed to be a promise. Therefore, knowing that something is an object—or even knowing a partial structural type—does not prove that it is not promise-like.This complicates types such as:
At runtime, deciding whether an arbitrary object is promise-like is already heuristic: the usual test inspects a
thenproperty. The static type system faces the analogous open-world problem. Unless a type is closed, absence of a declared property is not necessarily evidence of actual absence.Fresh object literals provide one escape hatch, since excess-property knowledge makes them temporarily closed enough for certain proofs. Negated index signatures may provide another by expressing that all unlisted keys are
never. The discussion suggests that negated types and closed-object modeling are closely related, but negation alone does not solve structural openness.A final edge case asks what it means to negate an object consisting entirely of optional properties:
Such a type is nearly useless as a refinement under open structural typing. Intersecting another object with
{ p?: string }usually just adds an optional property; it rarely producesnever. If the positive type overlaps almost every object, its negation cannot conclusively filter much. This example underscores that the usefulness ofnot Tdepends on the compiler being able to prove disjointness, not merely on syntactically constructing a complement.7.
Extract,Exclude,NonNullable, and existing type factsNegated types offer an appealing algebra for existing utility operations:
Their union should ideally reconstruct the original type:
Today,
ExtractandExcludeare exposed as distributive conditional types, though internal compiler operations sometimes use intersections instead of constructing the utility alias.NonNullable<T>has also moved toward an intersection-based implementation. Negation could make these operations more symmetric at the compiler's foundational level even if the public aliases remained conditional types for compatibility.The same observation applies to
{}. Under strict null checking,{}is commonly used to mean any non-nullish value, despite its misleading object-like spelling. Negation could represent that meaning directly:A substantial amount of TypeScript's current type-fact machinery exists to reconstruct, decompose, and recombine these special domains. A mature negation primitive could replace some of those rules with ordinary intersections and simplification laws.
However, rewriting existing library aliases or internal facts is precisely where compatibility risk appears. Even mathematically equivalent types can differ in display, inference, distributivity, alias preservation, or assignability edge cases. The discussion therefore favors measuring each prospective substitution independently rather than replacing everything at once.
8. Readability, simplification, and performance
Maximal precision is not automatically desirable in editor tooling. After dozens of checks, a technically exact hover containing dozens of intersections and negations is less useful than a recognizable named type. One participant explicitly favored preserving readable identifiers over exposing the full derivation of a control-flow result.
The selective design addresses both readability and performance:
The implementation nevertheless includes special reduction behavior. When an intersection contains a negated object type with discriminant properties, the checker can inspect those discriminants and filter incompatible constituents from a union. Ordinary intersections do not always eagerly perform this reduction. Negation supplies a strong signal that filtering is intentional, so the implementation performs additional work.
This makes the feature more expressive but creates performance questions for constructions involving two very large unions. The compiler has extensive existing optimizations for unions, intersections, and discriminant reduction, but large-scale measurements remain necessary.
9. Agreed direction and next experiments
The meeting does not reach a final language design. It does reach broad agreement that the explicit type constructor now has considerably more independent value than it did in earlier proposals. Template-literal types, index signatures, non-promise constraints, non-void callbacks, nonzero values, nonempty strings, and pseudo-closed object shapes all provide useful applications even without pervasive inference.
The unresolved question is how much of the existing type system should immediately be rebuilt around the new primitive.
The proposed next step is empirical experimentation:
The overall conclusion is cautiously favorable. Negated types appear powerful enough to justify continued work, but they are not merely another isolated type operator. Once present, they offer a more principled representation for several areas currently implemented through special cases. That creates pressure to unify control flow, utility operations, nullish handling, and structural-object reasoning around them. The design challenge is to obtain that coherence without making existing programs slower, less readable, or unexpectedly invalid—and without shipping an intentionally partial model that the language can never later revise.
LukeAbby commentedon Sep 2, 2026
What about the
typeRelatedToDiscriminatedTypespecial case? Basically this allows{ type: "a" | "b" }to be assignable to{ type: "a" } | { type: "b" }. I think this can cause a problem here.LukeAbby commentedon Sep 2, 2026
Bonus issue:
This one happens because the fresh literal
{ a: 1 }is allowed to widen to{ a: number }but still hits the fresh literal path that ends up accepting this.Currently
const _ok: not { a: 1 } = { a: 2 };is essentially accepted by coincidence (it hits that samenumberwidening). So something will have to prevent the widening. One route would be contextual typing. Though that raises some interesting questions like:Currently there doesn't appear to be an attempt to contextually type this. Also it seems like it'll interact poorly with function overloading.