GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Fix #24193: Allow nullable discriminants by jack-williams · Pull Request #27631 · microsoft/TypeScript

Conversation

@jack-williams

@jack-williams Jack Williams (jack-williams) commented Oct 9, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

Fixes #24193

This is technically a breaking change because it enables excessing property checking for certain kinds of unions (with null discriminant), when the check was previously disabled.

Example:

type Foo = { a: null; b: string } | { a: string, c: number };
const x: Foo = { a: null, b: "foo", c: 4};

Before: No error
Now:

Type '{ a: null; c: number; b: string; }' is not assignable to type 'Foo'.
  Object literal may only specify known properties, and 'c' does not exist in type '{ a: null; b: string; }'. [2322]

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

KSXGitHub, j-oliveras, alexrock, and RyanCavanaugh reacted with thumbs up emoji
All reactions
  • 👍 4 reactions
@jack-williams
Allow nullable discriminants
f53fad6
@jack-williams Jack Williams (jack-williams) changed the title Allow nullable discriminants Fix #24193: Allow nullable discriminants Oct 9, 2018
@jack-williams Jack Williams (jack-williams) mentioned this pull request Oct 9, 2018
Allow to use null as discriminator for tagged union #24193
Closed
4 tasks done
@RyanCavanaugh
Ryan Cavanaugh (RyanCavanaugh) requested a review from Anders Hejlsberg (ahejlsberg) October 9, 2018 22:19
jack-williams
Jack Williams (jack-williams) commented Oct 9, 2018
View reviewed changes
Comment thread src/compiler/checker.ts
hasDiscriminant = true;
continue;
}
else if (current.flags & (TypeFlags.DisjointDomains | TypeFlags.Object)) {

@jack-williams Jack Williams (jack-williams) Oct 9, 2018

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

As a signpost for Anders Hejlsberg (@ahejlsberg), I wasn't sure about this flag combination (TypeFlags.DisjointDomains | TypeFlags.Object). I was mostly concerned about avoiding unions that had a generic parameter that might be instantiated to something nullable, but felt it was safer to be positive about what is accepted, rather than negative about what is not. I also considered whether it would be just sufficient to check the union has at least one nullable and at least one known non-nullable.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Oct 10, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Let me propose a simpler and more general approach.

We currently consider a property of a union type to be a discriminant property if it has a non-uniform type and that type is a union of only unit types. I think we could slightly relax this to say it must be a union containing at least one unit type. This would allow us to discriminate in other cases such as

type Result<T> = { error?: undefined, value: T } | { error: Error };

function test(x: Result<number>) {
    if (!x.error) {
        x.value;
    }
    else {
        x.error.message;
    }
}

test({ value: 10 });
test({ error: new Error("boom") });

I put up the proposed (very simple) change in a branch named mixedDiscriminantTypes (see here). There is only one minor difference in the test baselines and one minor difference in the RWC tests, so I think it is workable.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jack-williams

Jack Williams (jack-williams) commented Oct 10, 2018

Copy link
Copy Markdown
Collaborator Author

Anders Hejlsberg (@ahejlsberg) I like your approach better (but to be pedantic your example does work with my fix). For your suggestion I think we need an additional constraint that says the union does not contain an instantiable type. Without so, the wrong narrowings are produced. For example, I think your fix produces the following behaviour:

type GenericA<T> = { a: 3; c: string } | { a: T; c: number };
function narrowT<T>(x: GenericA<T>) {
    if (x.a === 3) {
        // x: { a: 3; c: string }
        const aString: string = x.c; // ok, probably shouldn't be
    }
}
narrowT({ a: 3, c: 4 });

So a proposed alternative is:

  • it must be a union containing at least one unit type and no instatiable types

However this approach does have one drawback in that it prevents some useful narrowings:

type GenericA<T> = { a: 3; c: string } | { a: T; c: number }
function narrowT<T>(x: GenericA<T>) {
    if (x.a !== 3) {
        // in master (or my change) x: Generic<T>
        // the assignment is not ok, but probably should be
        // ok, in mixedDiscriminantTypes
        const aNumber: number = x.c;
    }
}
narrowT({ a: 3, c: 4 });

I think maybe the root of the problem is that the filtering done by discriminant narrowing uses the comparable relation, which does not relate type parameters to literals [1]. This filters out types with generic discriminates which shouldn't be filtered unless their base constraint is not comparable.

Changing the comparable relation seems really scary, though I would argue a generic T should be comparable to a unit type unless the base constraint of T is definitely not comparable.

Not sure how to proceed, but here is a summary:

  • I like your solution better but we should make sure to not filter union members that could be instantiated to a matching type. The simple fix would be rejecting a union with an instantiable type.
  • The more complete solution seems to be changing the filtering done by narrowTypeByDiscriminant, either by changing the filter, or changing the comparable relation. I'm starting to get abit out of my depth at this point, so I defer to your judgment (and anyone else's on this for that matter).
  • I'm happy to proceed however: whether you just want to want to merge your branch and drop this, or pull changes into this.

[1] I'm not sure of the full details of the comparable relation, I used the behaviour of the following to guide me:

function foo<X>(x: X) {
  if (x === 3) { // error
    
  }
}
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Oct 10, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

I think we need an additional constraint that says the union does not contain an instantiable type.

Good point. I think that would be a reasonable rule to add.

I think maybe the root of the problem is that the filtering done by discriminant narrowing uses the comparable relation, which does not relate type parameters to literals [1]. This filters out types with generic discriminates which shouldn't be filtered unless their base constraint is not comparable.

Yes, that's the root of the problem. We only allow comparing type parameters to undefined, null, and compatible type parameters. Since an unconstrained type parameter could be anything, it would technically be correct to allow it to be compared to anything. Yet, the current rules are very helpful in preventing inadvertent mixing of apples and bananas in expressions, and for that reason I think they're worth keeping.

I will put up a PR later today.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jack-williams

Jack Williams (jack-williams) commented Oct 10, 2018

Copy link
Copy Markdown
Collaborator Author

Sounds good! I'll close this issue up then.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@jack-williams Jack Williams (jack-williams) closed this Oct 10, 2018
@jack-williams

Jack Williams (jack-williams) commented Oct 10, 2018 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator Author

Would it be sufficient to only exclude unconstrained type parameters?

To answer my own question: no.

function foo<X extends {}, Y extends unknown>(x: X, y: Y) {
  x === 3; // err
  x === true; // err
  y === 3; // err
  y === true; // err
}
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@ahejlsberg Anders Hejlsberg (ahejlsberg) mentioned this pull request Oct 10, 2018
Allow non-unit types in union discriminants #27695
Merged
@ahejlsberg

Anders Hejlsberg (ahejlsberg) commented Oct 10, 2018

Copy link
Copy Markdown
Member

Jack Williams (@jack-williams) PR is up in #27695.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@watsonsong WatsonSong (watsonsong) mentioned this pull request Jun 2, 2022
Allow more types in discriminated unions colinhacks/zod#1075
Closed
@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 21, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@ahejlsberg Anders Hejlsberg (ahejlsberg) Awaiting requested review from ahejlsberg

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@jack-williams @ahejlsberg
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.