Skip to content

Suggestion: one-sided or fine-grained type guards #15048

Open
@mcmath

Description

@mcmath

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions