Skip to content

Add support for type guarding to if statements #53201

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

Open
5 tasks done
brandonmcconnell opened this issue Mar 10, 2023 · 10 comments
Open
5 tasks done

Add support for type guarding to if statements #53201

brandonmcconnell opened this issue Mar 10, 2023 · 10 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

@brandonmcconnell
Copy link

brandonmcconnell commented Mar 10, 2023

Suggestion

πŸ” Search Terms

type guard guarding assertion

βœ… 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

This proposal introduces a new feature that would allow developers to use type guard statements with if-statements rather than only with functions. This feature would provide a more concise and expressive syntax for type guards, making code easier to read and maintain.

πŸ“ƒ Motivating Example

With the proposed feature, developers could use the is keyword directly within an if-statement to perform a type guard. For example…

Before β€” requires separate function for type guarding

function isIterable<T>(obj: Iterable<T> | ArrayLike<T>): obj is Iterable<T> {
  return typeof (obj as Iterable<T>)[Symbol.iterator] === 'function';
}

function iterateOver<T>(obj: Iterable<T> | ArrayLike<T>, expose: Function) {
  if (isIterable(obj)) {
    // some logic here
  } else {
    // some logic here
  }
}

After β€” can handle type guarding inline via if statement

function iterateOver<T>(obj: Iterable<T> | ArrayLike<T>, expose: Function) {
  const isIterable = typeof (obj as Iterable<T>)[Symbol.iterator] === 'function';
  if (isIterable): obj is Iterable<T> {
    // some logic here
  } else {
    // some logic here
  }
}

** this could just as easily be rewritten to get rid of the isIterable placeholder altogether, like this:

function iterateOver<T>(obj: Iterable<T> | ArrayLike<T>, expose: Function) {
  if (typeof (obj as Iterable<T>)[Symbol.iterator] === 'function'): obj is Iterable<T> {
    // some logic here
  } else {
    // some logic here
  }
}

πŸ’» Use Cases

Currently, if developers want to use a type guard to narrow down the type of a value within an if statement, they need to define a separate function (AFAIK) that returns a boolean value indicating whether the value is of a certain type. This approach can be cumbersome and can lead to code that is harder to read and understand.

@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 Mar 10, 2023
@whzx5byb
Copy link

Duplicate of #41710

@brandonmcconnell
Copy link
Author

Head & open issue: #6474

@koshic
Copy link

koshic commented Mar 11, 2023

@brandonmcconnell theoretically it's a good idea, to remove function-only guards limitation. In the reality, more or less meaningful check + 'obj is' + type name will lead to extremely long, unreadable 'if' statements: instead of read guard name and go next you have to go trough visual garbage and find 'is xxx' statement to understand code logic. As a result, we will have language feature doomed to be permanently banned in various linters / style guides due to above reason.

@brandonmcconnell
Copy link
Author

@koshic I don't think the syntax would be banned as you mentioned. I could certainly see the syntax being used in ways that make expressions harder to read but that's not so different from any convention in JS or TS, or programming general for that matterβ€” ternaries, overrides, etc.

In cases where an if-statement might get too long, it would likely be recommended to move the condition into its own variable as I did in my first "after" example, above:

function iterateOver<T>(obj: Iterable<T> | ArrayLike<T>, expose: Function) {
  const isIterable = typeof (obj as Iterable<T>)[Symbol.iterator] === 'function';
  if (isIterable): obj is Iterable<T> {
    // some logic here
  } else {
    // some logic here
  }
}

@koshic
Copy link

koshic commented Mar 12, 2023

@brandonmcconnell yeah, it will work - but there is no real difference between

if (isIterable): obj is Iterable<T> {
  // some logic here
} else {
  // some logic here
}

and

if (isIterable(obj) {
  // some logic here
} else {
  // some logic here
}

Also, 'dedicated variable' pattern will split guard into 2 independent parts, which is unsafe: variable declared without any information about expected type. Obviously, check logic & expected type should be defined inside single syntax construct (guard function, or extended 'if' statement from your proposal). So, the chain will be a bit longer: 'do not use long guards in conditions -> extract them to variables -> keep variables near appropriate condition -> do we really need 3 eslint rules for that feature? let's ban it instead'.

What I want to say - every new feature should be fine balanced: flexibility, readability, maintainability & safety (== foolproof) are equal important. From my standpoint, ability to mark boolean variable with 'obj is Xxx' (and check that metadata in conditions) is more realistic: no visual garbage near conditions, single construct, very similar to existing guard functions.

Thx!

@brandonmcconnell
Copy link
Author

Great feedback. I appreciate you putting so much time into this.

My only counter-thought would be that the same vulnerability you mentioned is also present with functional type guards.

@koshic
Copy link

koshic commented Mar 12, 2023

Hmm, we can't separate guard function definition and 'o is Xxx' statement. Or may be I missed something obvious?

@brandonmcconnell
Copy link
Author

Ah I wasn't aware of this error. Got it

image

@matthew-dean
Copy link

matthew-dean commented May 16, 2023

I think this is a good idea, but related to #54270, you may want the ability to narrow within the else statement as well (or instead of), so I think this should be supported:

if ([condition]) {
    
} else: obj is Iterable<T> {

}

In addition, I would expand this to all scopes, because you should also be able to do:

while ([condition]): obj is Iterable<T> {
}

and:

do: obj is Iterable<T> {
} while ([condition])
// OR?
// do {
// } while ([condition]): obj is Iterable<T>

This breaks down with ternary statements, but at that level it adds too much complexity, and the as keyword should probably just be used there.

@matthew-dean
Copy link

If you don't want to complicate all the above control flow statements, you should just be able to do:

do {
  assert obj is Iterable<T>
} while ([condition])

if ([condition]) {
    
} else {
  assert obj is Iterable<T>
}

Right now, it's sometimes very complicated getting TypeScript to magically narrow a type within a scope. Ideally this could just be triggered manually.

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

5 participants