Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

De-narrowing of never with postfix ! #51097

Closed
5 tasks done
RyanCavanaugh opened this issue Oct 7, 2022 · 8 comments
Closed
5 tasks done

De-narrowing of never with postfix ! #51097

RyanCavanaugh opened this issue Oct 7, 2022 · 8 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Oct 7, 2022

Suggestion

πŸ” Search Terms

never postfix narrowing unnarrowing

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Postfix !, aka the non-null assertion operator, should be overloaded so that it can undo narrowings to never. Simply, when an expression of type never is the operand of !, TypeScript should instead evaluate it as its declared type.

πŸ“ƒ Motivating Example

Discriminator exhaustion

type AorB = { kind: "A" } | { kind: "B" };
function fn(x: AorB) {
    switch (x.kind) {
        case "A":
            console.log("It is A");
            break;
        case "B":
            console.log("It is B");
            break;
        default:
            throw new Error("Bad kind: " + x.kind);
            //                             ^^^^^^^
            // Error - no 'kind' property on 'never'
    }
}

We should be able to use ! to allow access to the kind property:

            throw new Error("Bad kind: " + x!.kind);

πŸ’» Use Cases

Feature detection - #51059

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Oct 7, 2022
@RyanCavanaugh
Copy link
Member Author

Implementing this, some funny stuff happens. Let's say in the OP example you write

type AorB = { kind: "A" } | { kind: "B" };
function fn(x: AorB) {
    switch (x.kind) {
        case "A":
            console.log("It is A");
            break;
        case "B":
            console.log("It is B");
            break;
        default:
            const p = x!.kind; // <-- πŸ‘€
    }
}

What type should p get here?

Naively, maybe "A" | "B"?

What actually happens is that we fetch the symbol for x, resolve it to its declared type, and then narrow the kind property according to the switch, resulting in p having the inferred type never.

The interesting thing about this is that the naive type "A" | "B" is, by one argument, the very most wrongest type we could possibly give p -- "A" and "B" are the two values in the entire universe we know it's not. So the produced type never here is seemingly much more accurate, even if surprising.

Naturally (?) at that point you can write

const p = x!.kind!;

to get p: "A" | "B", which is sensical enough.

Are there other use cases for this?

@shicks
Copy link
Contributor

shicks commented Oct 7, 2022

This would make ! no longer idempotent, which is maybe a little surprising?

declare function what<T>(arg: T): void;
const x = null;
what(x!);
// ^?
what(x!!);
// ^?

@RyanCavanaugh
Copy link
Member Author

RyanCavanaugh commented Oct 7, 2022

I suppose, though it would only be non-idempotent for values with a declared type of null/undefined/null | undefined, which is hopefully rare. In the PR I made it so that x! is still NonNullable<the declared type of x> to avoid having to write x!! in "real" code.

@shicks
Copy link
Contributor

shicks commented Oct 8, 2022

That would at least address the non-idempotency: in my example, x!! would still be never. It still may be a little weird that ! means two quite different things, and that which it is depends on some very hidden context. If a new enum case were added to your example, for instance, then all of a sudden x! goes from being the declared type to being the third case. That may not be a problem in this example, but maybe there are situations where it could be troublesome? This is a little contrived, but

function f(arg: number|string) {
  if (typeof arg === 'number') {

  } else if (typeof arg === 'string') {

  } else {
    console.error(`Unknown argument: ${arg!.toString()}`);
  }
}

Here, we un-narrow arg since toString isn't defined on never. Now suppose over time, arg changes to number|string|boolean (no compile errors, though we might start seeing the console message and maybe a lint warning for an unnecessary !-assertion) and then is made optional. Now it's narrowed to boolean|undefined in the else branch, and we miss the null-dereference error because arg! has become a non-null assert rather than an un-narrow.

I wonder if rather than reusing !, it wouldn't be better to stretch it out a bit to something along the lines of as declared or as declaredTypeOf x or something? This could also allow un-narrowing in other situations as well (when it's not narrowed all the way down to never) and would allow making use of that type.

@Jack-Works
Copy link
Contributor

This is strange, I don't like this idea of reverting never type. If you exclude all possibilities, you should not have any other type than "never".

The access to an inferred "never" variable should always be like "(node as any).kind".

1 similar comment
@Jack-Works
Copy link
Contributor

This is strange, I don't like this idea of reverting never type. If you exclude all possibilities, you should not have any other type than "never".

The access to an inferred "never" variable should always be like "(node as any).kind".

@shicks
Copy link
Contributor

shicks commented Oct 11, 2022

Thinking more about this, I'm very concerned with having the same operator both narrowing (the existing usage) and widening (i.e. de-narrowing).

@RyanCavanaugh
Copy link
Member Author

Seems like not much interest / use cases for this

@RyanCavanaugh RyanCavanaugh closed this as not planned Won't fix, can't repro, duplicate, stale Oct 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants