Typescript: allow storing results of narrowing in booleans for further narrowing

Created on 11 Jun 2018  ·  9Comments  ·  Source: microsoft/TypeScript

Search Terms

https://github.com/Microsoft/TypeScript/search?q=type+guards+constants+narrowing&type=Issues

Suggestion

There is no way to use a saved result of a type guard evaluation for narrowing. Allow narrowing by

  • storing assertions (booleans) of narrowing into constants
  • impose narrowing based on the values carried by those constants

Use Cases

  • reuse for narrowing evaluations, in case there are two places that narrow using the same guard on the same value
  • declutter "if" expressions
  • give names to narrowing facts for better readability

Examples

// now
            if (
                isLoneAvailableCapacity(feed) &&
                (isAvailableCapacitySubbeamPath(link) || isAvailableCapacityConnectivityLegPath(link)) &&
                toAvailableCapacityKey(feed.availableCapacity) === link.availableCapacityKey
            ) {


// could be
            const isAvailableCapacityPairdWithPath = isLoneAvailableCapacity(feed) &&
                (isAvailableCapacitySubbeamPath(link) || isAvailableCapacityConnectivityLegPath(link)) &&
                toAvailableCapacityKey(feed.availableCapacity) === link.availableCapacityKey

            if (isAvailableCapacityPairdWithPath) {
            }


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. new expression-level syntax)
Needs Proposal Suggestion

Most helpful comment

Instead of inferring boolean for the intermediate variable, the TypeChecker could infer a type predicate. That could look similar to type predicates in type guard functions. Type guard types would be a subtype of boolean and can therefore be used like any other boolean, but with the added benefit of narrowing another variable.

declare function isNumber(v: any): v is number;

function infer(x: string | number) {
  // currently inferred as 'boolean'
  // now inferred as 'x is number';
  const intermediate = isNumber(x);
  if (intermediate) {
    x.toFixed();
  } else {
    x.substr();
  }
}

// maybe also allow this syntax in type position
function syntactic(x: string | number) {
  const intermediate: x is number = doSomething();
  if (intermediate) {
    x.toFixed();
  } else {
    x.substr();
  }
}

Caveat: the inferred type predicate needs to be invalidated at the next assignment of the narrowed variable (x). When dealing with object types this becomes more difficult, because assigning a property could also invalidate the type predicate.

All 9 comments

looks like you want the compiler to automatically infer dependent types for you :)

i merely ask to extend booleans (in the local scope) with knowledge of what's been narrowed

If I understand it correctly this is similar to #24983 ? I don't see a typeof keyword here but is that what this is about?

like this? :

function twostep(x: symbol | string) {
    const isSymbol = typeof x === "symbol";
    if (!isSymbol) {
        console.log(`Type of x is still string | symbol here: ${x}`);
    }
}

function onestep(x: symbol | string) {
    if (typeof x !== "symbol") {
        console.log(`Type of x is now just string: ${x}`);
    }
}

Instead of inferring boolean for the intermediate variable, the TypeChecker could infer a type predicate. That could look similar to type predicates in type guard functions. Type guard types would be a subtype of boolean and can therefore be used like any other boolean, but with the added benefit of narrowing another variable.

declare function isNumber(v: any): v is number;

function infer(x: string | number) {
  // currently inferred as 'boolean'
  // now inferred as 'x is number';
  const intermediate = isNumber(x);
  if (intermediate) {
    x.toFixed();
  } else {
    x.substr();
  }
}

// maybe also allow this syntax in type position
function syntactic(x: string | number) {
  const intermediate: x is number = doSomething();
  if (intermediate) {
    x.toFixed();
  } else {
    x.substr();
  }
}

Caveat: the inferred type predicate needs to be invalidated at the next assignment of the narrowed variable (x). When dealing with object types this becomes more difficult, because assigning a property could also invalidate the type predicate.

I'd love to see this worked on. The motivation for me is this:

give names to narrowing facts for better readability

Code readability is very important; at present the behaviour of type narrowing enforces a code style which doesn't allow for meaningful variable names to drive control flow whilst maintaining narrowed types. I super miss that.

Completely agree, I've found myself wanting to do this a lot as I like keeping the main body of logic as simple and human readable as possible.
A simple example is (where add6 causes an error):

   function add4(n: unknown): number | undefined {
      if (typeof n === "number") {
         return n + 4;
      } else {
         return undefined;
      }
   }

   function add6(n: unknown): number | undefined {
      const nIsNumber = typeof n === "number";
      if (nIsNumber) {
         return n + 6;
      } else {
         return undefined;
      }
   }

The way I can see of resolving it is (as @ajafff said) to make type predicates (x is y) into proper types, so in add6 "nIsNumber" would be of type "n is number" rather than type "boolean".

Note that this example is simplified to illustrate the problem but isn't a case where it would be particularly necessary.

It'd be great if TS solved this problem. I strongly agree with @johnnyreilly that this limitation causes less-readable code.

Note that the problem is broader than TypeScript-specific constructs like typeof and user-defined type guards. It also complicates migrating existing JavaScript code to TS when using common JS idioms like (example 2 below) preventing property access on a may-be-null object. It even breaks switch statements (example 3 below) where there's no intermediate local variables at all!

Also, the problem doesn't only apply to boolean values. It applies to values of any type that may be used in a truthy/falsy context, like examples 2 and 3 below.

function repro (a: string[] | null) { 

  // example 1 (no error)
  if (a && a.length===1) {
    console.log (a.length); // no compiler error
  }

  // example 2 (error)
  const test = a && a.length===1;
  if (test) {
    console.log (a.length); // compiler error: Object is possibly null
  }

  // example 3 (error)
  switch (a && a.length) {
    case 1: 
      console.log (a.length); // compiler error: Object is possibly null
    default: 
      return null;
  }

}

BTW, I think this is a dupe of #12184.

I find myself coming back here every few weeks and reading this issue and every related issue in hopes this will be implemented. I like typescript, but this is the most frustrating thing I've encountered using it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  ·  3Comments

uber5001 picture uber5001  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments