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

Support !== operator in is and asserts return type syntax #55813

Open
5 tasks done
MichaelMitchell-at opened this issue Sep 21, 2023 · 3 comments
Open
5 tasks done

Support !== operator in is and asserts return type syntax #55813

MichaelMitchell-at opened this issue Sep 21, 2023 · 3 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

@MichaelMitchell-at
Copy link

MichaelMitchell-at commented Sep 21, 2023

πŸ” Search Terms

unknown, assert, narrow, undefined, "is not"

βœ… Viability Checklist

  • 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 our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

⭐ Suggestion

This looks like something that would have been asked before, but it's hard to search for.

Right now asserts is a pretty magical syntax that lets you do:

function assert(proposition: unknown): asserts proposition {
    if (!proposition) {
        throw new Error()
    }
}

function foo(value: unknown): {} | null {
    assert(value !== undefined);
    return value;
}

However, trying to express a more specialized function doesn't work

function assertNotUndefined<T>(value: T): asserts value is Exclude<T, undefined> {
    if (value === undefined) {
        throw new Error();
    }
}

function foo(value: unknown): {} | null {
    assertNotUndefined(value);
    return value;  // Doesn't work
}

This is because Exclude<unknown, undefined> is still just unknown, since unknown is not just an alias for {} | null | undefined. Assuming that won't change, how else can one express assertNotUndefined? Well, if we take the original assert function and try to retroactively rationalize its syntax as this

function assert(proposition: unknown): asserts proposition;

when called as

assert(proposition !== undefined)

(hand-waving) just specializes to this

function assert(proposition: unknown): asserts proposition !== undefined;

I don't care to bikeshed over whether the syntax should be asserts proposition is not undefined, whether the LHS or RHS operands can be flipped, whether != or other operators should exist, or if any type expressions other than primitive literals like undefined should be supported, so I'll leave that to the comments.

πŸ“ƒ Motivating Example

function assertNotUndefined(proposition: unknown): asserts proposition !== undefined;

function foo(value: unknown): {} | null {
    assertNotUndefined(value);
    return value;  // Works!
}

πŸ’» Use Cases

  1. What do you want to use this for?
    A function that can exclude undefined can be composed with other functions, e.g. array.filter(isNotUndefined)
  2. What shortcomings exist with current approaches?
    There's no clean way to define such a function without ugly conditionals to handle unknown.
  3. What workarounds are you using in the meantime?
    Rewrite code in more verbose style, e.g. manually construct a new array using a for loop instead of array.filter.
@guillaumebrunerie
Copy link

How about this?

function assertNotUndefined<T extends {} | null>(value: T | undefined): asserts value is T {
    if (value === undefined) {
        throw new Error();
    }
}

Seems to work fine, as unknown is assignable to {} | null | undefined.

@MichaelMitchell-at
Copy link
Author

How about this?

function assertNotUndefined<T extends {} | null>(value: T | undefined): asserts value is T {
    if (value === undefined) {
        throw new Error();
    }
}

Seems to work fine, as unknown is assignable to {} | null | undefined.

I think that's sufficient solution for this use case and one I'll likely end up using :).

In general I try to type parameters as just a generic type parameter, rather than a type derived from a type parameter, because I've learned not to trust TS's ability to infer the correct type for the type parameter for any non-trivial type. I find that when TS can and can't infer the correct type parameter from usage to be underspecified and something I just don't want to rely on.

@fatcerberus
Copy link

In the general case you probably need #4196 for this.

As for TS not being able to infer types, that's generally because the type system is structural so when your inference site is DerivedFrom<T> the compiler has to work backwards from the derived type to infer T. If the derivation is lossy, it may not even be possible to reliably recover T from it.

@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 Sep 26, 2023
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

4 participants