Repository navigation
Design Meeting Notes, 2026-08-27 #64113
Description
Activity
- addedDesign NotesNotes from our design meetingsNotes from our design meetings
on Aug 31, 2026 DanielRosenwasser commented
on Sep 1, 2026 MemberAuthorMore actionsAn 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 commented
on Sep 1, 2026 MemberAuthorMore actionsSomething 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 commented
on Sep 1, 2026 MemberAuthorMore actionsWe nerd-sniped Ryan Cavanaugh (@RyanCavanaugh) into talking about what the heck
not voidis.RyanCavanaugh commented
on Sep 1, 2026 MemberMore actionsHere's an AI summary of the discussion. The recording started a little bit late
Negated Types in TypeScript — Design Discussion Summary
The recording begins at the end of an explanation of the proposed subtype/assignability rules, so the discussion primarily covers use cases, control-flow integration, inference, compatibility, and implementation strategy rather than introducing the syntax from first principles.
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:S & not T
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:- One rule may require an explicitly marked discarded expression, such as
void expression. - Another may prohibit discarding a promise without awaiting or otherwise handling it.
A parameter accepting "anything except a promise" could express the intended policy directly:
declare function discard<T extends not PromiseLike<unknown>>(value: T): void;
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:
T extends not void T extends not null T extends not undefined
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:
number & not 0 string & not ""
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:
string & not keyof T
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:type Closed<T> = T & { [K in string & not keyof T]: never };
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:
Node & not Identifier & not FunctionDeclaration & not ClassDeclaration // ...
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:
- The comparison is suitable for exact exclusion, such as comparison with a known literal.
- The relevant branch is one where a negation provides useful information.
- The use site has a contextual type containing a negation, indicating that this precision is actually required.
For example:
declare function divide( numerator: number, denominator: number & not 0 ): number; declare const x: number; if (x !== 0) { divide(10, x); }
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:
if (x !== 0) { const y = x; divide(10, y); // May lose the `not 0` refinement. }
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:
function requireNonNull(x: unknown) { if (x === null) throw new Error(); return x; }
In a maximally precise system, the inferred return type might be:
unknown & not null
Likewise, a runtime truthiness assertion might return something approximating:
unknown & not null & not undefined & not false & not 0 & not 0n & not ""
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:
- Adding an explicitly written negated type constructor.
- Retrofitting negations into control flow, inference, utility types, and the standard library.
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:
- retain negated refinements through more bindings;
- infer them in return types and predicates;
- use them in the false branch of conditional types;
- replace special nullish and type-fact behavior;
- make extraction and exclusion symmetric;
- strengthen standard-library signatures.
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:
type Result<A, B> = A extends B ? TrueBranch<A> : FalseBranch<A>;
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:
A & B = never
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:
number & not 0 string & not ""
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:
object & not PromiseLike<unknown>
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:
not { p?: string }
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:
Extract<T, U> ≈ T & U Exclude<T, U> ≈ T & not U
Their union should ideally reconstruct the original type:
(T & U) | (T & not U) ≈ T
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:unknown & not null & not undefined
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:
- Negations are constructed only when demanded.
- Fresh flow negations disappear at widening points.
- Users who never write a negated type generally do not see one.
- Long chains of irrelevant exclusions do not accumulate.
- Relationship work is avoided unless the negation affects a check.
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:
- Run the conservative implementation against a large corpus of popular TypeScript projects.
- Compare it with variants that retain or infer negations more aggressively.
- Test individual compatibility-sensitive changes separately, including conditional-type false branches, standard-library annotations, utility-type internals, and nullish type facts.
- Distinguish superficial baseline churn—such as changed hover text—from meaningful user-code failures.
- Examine real requests for negated types and patch representative library declarations to see how downstream programs behave.
- Use those results to determine whether a coherent default is possible or whether stronger semantics require an opt-in mode.
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.
Reacted by snarbles2 and Igor Oleinikov- One rule may require an explicitly marked discarded expression, such as
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.type A = { kind: "a" }; type B = { kind: "b" }; declare const ab: "a" | "b"; const aOrB: A | B = { kind: ab }; // Ok const notAOrB: not (A | B) = { kind: ab }; // Ok
Bonus issue:
const _oops: not { a: 1 } = { a: 1 }; // Ok
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:const _f: not ((x: number) => string) = (arg) => doStuff(x) // should `arg` should contextually type as `not number`? // should the return should contextually type as `not string`?
Currently there doesn't appear to be an attempt to contextually type this. Also it seems like it'll interact poorly with function overloading.
Negated Types (
not T)#63926
AandBoverlapping whereA & Bis the overlap,not Ais all the area outside ofA,not (A & B)is the inverse color of that original overlap.Something like a
--noUnusedReturnedValuesDon't want to forget
People use
void, but this acceptsPromises that need to beawaited.Can have an
ignoreto help here.`foo${string & not "reserved-key"}`Record<`on${string & not keyof EventNames}`, unknown>Can also do narrowing for carve outs of infinite sets.
objectdoesn't necessarily exclude aPromiseor aFunction.nots in the default, whether you cared or not. This PR avoids that unless you actually need the negation.not Promisenots in hover unless you need.objectequivalent to{} & not number & not string & not boolean & not symbol?{}equivalent tonot null & not undefined?ExtractandExcludecan be defined in this way?not { optionalProp?: Type }