question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Suggestion: one-sided or fine-grained type guards

See original GitHub issue

The Problem

User-defined type guards assume that all values that pass a test are assignable to a given type, and that no values that fail the test are assignable to that type. This works well for functions that strictly check the type of a value.

function isNumber(value: any): value is number { /* ... */ }

let x: number | string = getNumberOrString();

if (isNumber(x)) {
  // x: number
} else {
  // x: string
}

But some functions, like Number.isInteger() in ES2015+, are more restrictive in that only some values of a given type pass the test. So the following does’t work.

function isInteger(value: any): value is number { /* ... */ }

let x: number | string = getNumberOrString();

if (isInteger(x)) {
  // x: number (Good: we know x is a number)
} else {
  // x: string (Bad: x might still be a number)
}

The current solution – the one followed by the built-in declaration libraries – is to forgo the type guard altogether and restrict the type accepted as an argument, even though the function will accept any value (it will just return false if the input is not a number).

interface NumberConstructor {
  isInteger(n: number): boolean;
}

A Solution: an “as” type guard

There is a need for a type guard that constrains the type when the test passes but not when the test fails. Call it a weak type guard, or a one-sided type guard since it only narrows one side of the conditional. I would suggest overloading the as keyword and using it like is.

function isInteger(value: any): value as number { /* ... */ }

let x: number | string = getNumberOrString();

if (isInteger(x)) {
  // x: number
} else {
  // x: number | string
}

This is only a small issue with some not-too-cumbersome workarounds, but given that a number of functions in ES2015+ are of this kind, I think a solution along these lines is warranted.

A more powerful solution: an “else” type guard

In light of what @aluanhaddad has suggested, I feel the above solution is a bit limited in that it only deals with the true side of the conditional. In rare cases a programmer might want to narrow only the false side:

let x: number | string = getNumberOrString();

if (isNotInteger(x)) {
  // x: number | string
} else {
  // x: number
}

To account for this scenario, a fine-grained type guard could be introduced: a type guard that deals with both sides independently. I would suggest introducing an else guard.

The following would be equivalent:

function isCool(value: any): boolean { /* ... */ }
function isCool(value: any): true else false { /* ... */ }

And the following would narrow either side of the conditional independently:

let x: number | string = getNumberOrString();

// Narrows only the true side of the conditional
function isInteger(value: any): value is number else false { /* ... */ }

if (isInteger(x)) {
  // x: number
} else {
  // x: number | string
}

// Narrows only the false side of the conditional
function isNotInteger(value: any): true else value is number { /* ... */ }

if (isNotInteger(x)) {
  // x: number | string
} else {
  // x: number
}

For clarity, parentheses could optionally be used around one or both sides:

function isInteger(value: any): (value is number) else (false) { /* ... */ }

At this point I’m not too certain about the syntax. But since it would allow a number of built-in functions in ES2015+ to be more accurately described, I would like to see something along these lines.

Issue Analytics

  • State:open
  • Created 6 years ago
  • Reactions:58
  • Comments:28 (6 by maintainers)

github_iconTop GitHub Comments

15reactions
iansan5653commented, Dec 8, 2020

From #36275, another use case of this is for Array.includes:

const arr: number[] = [1,2,3,4]

function sample(x: unknown): void {
  if(arr.includes(x)) {
    // x is definitely a number
  } else {
    // x may or may not be a number
  }
}
9reactions
mcmathcommented, Apr 25, 2017

Three kinds of use case

@RyanCavanaugh: I agree the extra complexity would be unwarranted if there were too few practical use cases. There are a lot of cases where predicate functions could be more accurately described with this proposal; but such accuracy may not be necessary in many cases.

That said, there are three general kinds of case where this kind of type guard could be put to use:

  • describing new predicate functions introduced in ES2015,
  • describing changes to the behavior of ES5 functions introduced in ES2015,
  • describing custom functions whose behavior matches these built-in functions

I’m going to assume the else syntax in the examples below, but I’m not suggesting that should be the final syntax.

New ES2015+ predicate functions

The predicate functions added in ES2015 as static methods of the Number constructor accept any value, and return false when passed non-number values. TypeScript currently describes them as accepting only numbers:

function isNaN(value: number): boolean;
function isFinite(value: number): boolean;
function isInteger(value: number): boolean;
function isSafeInteger(value: number): boolean;

With this proposal, these could be described more accurately as follows:

function isNaN(value: any): value extends number else false;
function isFinite(value: any): value extends number else false;
function isInteger(value: any): value extends number else false;
function isSafeInteger(value: any): value extends number else false;

Changes to existing predicate functions in ES2015+

Along similar lines, ES2015 modifies the behavior of several static methods of the Object constructor initially introduces in ES5. TypeScript currently describes them as follows:

function isExtensible(value: any): boolean;
function isFrozen(value: any): boolean;
function isSealed(value: any): boolean;

These methods throw a TypeError when passed a non-object in ES5. Even in ES5, they should be described like so:

function isExtensible(value: object): boolean;

But in ES2015+, they return false when passed a primitive value. So with this proposal, they would be described as follows, but only when targeting ES2015 and above:

function isExtensible(value: any): value as object else false;

This kind of case is a bit more challenging than the first, as the TypeScript’s ES2015 declarations currently reference the ES5 declarations.

User-defined predicate functions

In keeping with the ES2015+ way of defining predicate functions, a TypeScript user might want to define any number of similar functions.

/**
 * Tests whether a value is a non-negative integer.
 * Non-numbers return false.
 */
function isWholeNumber(value: any): value is number else false;
/**
 * Tests whether a value is a string of length 1.
 * Non-strings return false.
 */
function isCharacter(value: any): value is string else false;
/**
 * Tests whether a value is an empty array.
 * Non-arrays return false.
 */
function isEmpty(value: any): value is any[] else false;
Read more comments on GitHub >

github_iconTop Results From Across the Web

How to use type guards in TypeScript
A type guard is a TypeScript technique used to get information about the type of a variable, usually within a conditional block.
Read more >
6 Managing Fine-Grained Access in PL/SQL Packages and ...
Configuring fine-grained access control for users and roles that need to access external network services from the database. This way, specific groups of...
Read more >
Documentation - Advanced Types
A type guard is some expression that performs a runtime check that guarantees the type in some scope. Using type predicates. To define...
Read more >
Occupational Safety and Health Standards for Agriculture
A ROPS used on track-type tractors shall meet the test and performance requirements of 29 ... (2) ASAE recommendation R313.1-1971, as reconfirmed in...
Read more >
Fine-Grained Privilege Separation for Web Applications
Fine -Grained Privilege Separation for Web Applications ... which is more fragile because a single security compromise can affect every user of the...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found