Suggestion: one-sided or fine-grained type guards
See original GitHub issueThe 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:
- Created 6 years ago
- Reactions:58
- Comments:28 (6 by maintainers)
Top GitHub Comments
From #36275, another use case of this is for
Array.includes
: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:
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:With this proposal, these could be described more accurately as follows:
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:These methods throw a TypeError when passed a non-object in ES5. Even in ES5, they should be described like so:
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:
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.