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

Allow inline type predicates #6474

Open
saschanaz opened this issue Jan 14, 2016 · 9 comments
Open

Allow inline type predicates #6474

saschanaz opened this issue Jan 14, 2016 · 9 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@saschanaz
Copy link
Contributor

saschanaz commented Jan 14, 2016

From #5731

if (<foo is Element>(foo.nodeType === 1)) { 
    // Assume foo is Element here
}

A proposal from @sandersn: #5731 (comment)

Type Predicate Expressions

A type predicate expression allows you to narrow a union type with a single expression. The syntax is

<variable is type> boolean-expression

When the expression is used in a conditional context, the true branch of the conditional narrows variable to type, while the false branch narrows variable by removing type.

The type predicate expression is of type Boolean, with apparent type of type predicate. The right hand side must be an expression of type Boolean. The left hand side's variable must be a name bound in the current scope. The left hand side's type must be a name bound in the current scope.

Open questions

  1. How does variable capture work? Can a type predicate expression be returned from a function and used to narrow a variable that's no longer in scope? This could be defined to cause an error, but that restriction is neither obvious nor easy to use.
  2. Why reuse the type assertion syntax? The type of the right expression is checked to be Boolean, unlike assertions, and the resulting type is still Boolean, also unlike assertions.
@pushkine
Copy link
Contributor

pushkine commented Sep 26, 2020

Here's a workaround that gets removed by bundlers

const Narrow = <T>(v): v is T => true;

if (foo.nodeType === 1 && Narrow<Element>(foo)) {
}

(edit 2022) Here's another workaround that works as a statement (= works in switch blocks)

function Narrow<T extends R, R = unknown>(value: R): asserts value is T {}

switch (foo.nodeType) {
	case 1: {
		foo; // any
		Narrow<Element>(foo);
		foo; // Element
	}
}

Playground Link

@jsejcksn
Copy link

Here's a workaround that gets removed by bundlers

const Narrow = <T>(v): v is T => true;

if (foo.nodeType === 1 && Narrow<Element>(foo)) {
}

Combined:

playground link

function narrow <T>(value: unknown, expr: unknown): value is T {
  return Boolean(expr);
}

declare const foo: Node;

if (narrow<Element>(foo, foo.nodeType === 1)) {
  foo; // Element
}

@jsejcksn
Copy link

if (<foo is Element>(foo.nodeType === 1)) { 
    // Assume foo is Element here
}

@saschanaz What would it look like using as syntax? Something like this?

if (foo.nodeType === 1 as foo is Element) {
  foo; // Element
}

@saschanaz
Copy link
Contributor Author

It should probably still be with parens - (foo.nodeType === 1), but basically I'd expect such form.

@steinybot
Copy link

steinybot commented Jun 1, 2022

Here is another workaround:

function isF<A, B extends A>(f: (a: A) => boolean): (a: A) => a is B {
  return ((a: A) => f(a)) as (a: A) => a is B
}

function filterUndefined<A>(as: (A | undefined)[]): A[] {
  return as.filter(isF((a) => a !== undefined))
}

@e9x
Copy link

e9x commented Jul 28, 2022

A better syntax might be:

if (foo.nodeType === 1): foo is Element {
    // Assume foo is Element here
}

This is similar to the declaration of type guards. However, maybe you have a pair of values that depend on eachother to know the type. An example of the original syntax, checking two "dependant" values:

if (<foo is Element>(foo.nodeType === 1) && <bar is Text>(foo.nodeType === 3)) { 

With my first suggestion, this would look like:

if (foo.nodeType === 1): foo is Element {
    // Assume foo is Element here

    if (bar.nodeType === 3): bar is Text {
        // Assume bar is Element here
    }
}

The original syntax beats a nested if statement but looks unclear. Nested if statements may be inefficient for a one off type guard.

Functions have only one output but have multiple inputs and type guards only address one input. We can fix this by allowing type guards to address multiple inputs:

function isPair(foo: Node, bar: Node): foo is Element, bar is Text;

if (isPair(foo, bar)) {
    // Assume foo is Element here
    // Assume bar is Text here
}

An example of my suggestion now:

if (foo.nodeType === 1 && bar.nodeType === 3): foo is Element, bar is Text  {
    // Assume foo is Element here
    // Assume bar is Element here
}

Although the current approach would to be create two type guards, there are scenarios where foo and bar may be dependant on eachother in order to determine their types (eg. foo must contain a string found in bar).

Example:

type HelloWorldFirst = Element;
type HelloWorldLast = Element;

// foo and bar derived from a parent element
// they are a pair
// const [foo, bar] = element.childNodes;

if (foo.textContent === "Hello, " && bar.textContent === "world!"): foo is HelloWorldFirst, bar is HelloWorldLast {
    // Assume foo is HelloWorldFirst here
    // Assume bar is HelloWorldLast here
    // We can now pass foo to a function that expects the element to only contain "Hello, " or be of type HelloWorldFirst
}

@jsejcksn
Copy link

jsejcksn commented Jul 28, 2022

Functions have only one output but have multiple inputs and type guards only address one input. We can fix this by allowing type guards to address multiple inputs:

function isPair(foo: Node, bar: Node): foo is Element, bar is Text;

if (isPair(foo, bar)) {
    // Assume foo is Element here
    // Assume bar is Text here
}

+1 for proper compound predicates

Aside: You can hack this now by packing the candidates into a tuple (and then unpacking):

Playground

declare function isPair (tuple: readonly [Node, Node]): tuple is [Element, Text];

declare const foo: Node;
declare const bar: Node;

const fooBarPack = [foo, bar] as const;

if (isPair(fooBarPack)) {
  const [
    foo,
  //^? const foo: Element
    bar,
  //^? const bar: Text
  ] = fooBarPack;
}

Of course (because it's a hack) it requires (wastefully) creating that tuple.

@ethanresnick
Copy link
Contributor

You could also imagine doing this as the annotation for a variable holding the predicate's result, i.e.:

declare const foo: Node;
const isElement: foo is Element = foo.nodeType === 1;

if (isElement) {
    // Assume foo is Element here
}

@brandonmcconnell
Copy link

The ideal API for this in my opinion would be something similar to the way that type guard functions assert types, but allowed in more places, like on if-statements. 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
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants