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;
}
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.
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.
Just ran into a case where I want exactly this. Strongly agree with this idea. Not sure about as vs. is being the syntactical distinguishing feature, but definitely want a one-sided type-guard in some fashion.
function isInteger(value: any): value as number { /* ... */ }
Just bikeshedding here but I would prefer a syntax that was either more intuitive or more explicit. The as might be familiar to C# programmers as a conditional reference conversion but the analogy is a stretch.
What about
function isInteger(value: any): (value is number) | false { /* ... */ }
@aluanhaddad Actually, I think something along those lines would be more powerful – not just more intuitive – since it would allow for independent control over both the true and false sides of the conditional.
I would suggest using else instead of | to separate each side, as the following could be confused, especially in more complicated cases with a lot of parentheses:
function isInteger(value: any): value is number | false;
function isInteger(value: any): (value is number) | false;
I've updated my suggestion in light of your comments.
@mcmath I like the idea of using else to reduce parentheses, but I was actually not proposing branching.
I was proposing
function isInteger(value: any): value is number else false
simply as the syntactic form for writing a one-sided type guard.
That said, I like where you went with it. It does indeed open up a lot of power.
I almost suggested | myself, but I like else a lot more. But ultimately I held off on the |/else suggestion because it's a kind of weird case where value is number implies value is number else not number which relies on type negation/subtraction (another feature I very, very much want). Basically, it's kind of confusing that the two-sided type-guard is the default.
It'd be good to collect some more use cases here.
The main objection from the design meeting was that once there are two different kinds of type guards, there's an additional cognitive load for people to choose one or the other correctly.
@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.
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;
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.
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;
I'm writing code where I frequently need to check whether a variable is a function or an object, but not null. I need that to decide whether I should use Map or WeakMap.
I wanted to move the type check to a type guard function, so that I don't have to write (typeof x === "object" || typeof x === "function") && x !== null over and over. However as the current type guards are only an implication of a type, not equivalence, I can't do it without making the types less strict.
I think that the x is T else false seems good to me, as the addition makes it clear it's stricter. People who don't need the fine-grained type guard don't even need to know it exists. Adding cognitive to programmers doesn't seem like a good reason not to implement it – either you use the "normal" type guard, or something doesn't work, you google it and change that to the "strict" type guard. Not much to think about.
It really bugs me when I design a module with really good, strict types and then I have to relax them because “not enough people need types this strict“, so the feature won't be implemented 🙁
Another related use case: an "isEmpty" function, like lodash's _.isEmpty would be more useful if a false result could indicate to the compiler that the param is not null | undefined.
Here's a current annoying behavior:
declare const someArray: number[] | undefined;
if (!_.isEmpty(someArray)) {
// compiler error: someArray may be undefined.
// requires a non-null assertion event though I know it's a non-empty array
console.log(someArray[0]);
}
A solution to this would require being able to specify the type guard in terms of a false result, rather than a true result. That would basically be the exact inverse of current custom type guards, but would not solve the OP's issue. A solution that takes care of both situations would be best.
NOTE: It is currently easy to implement the inverse of isEmpty as a type guard as follows:
export function isNonEmpty<T>(value: T | undefined | null): value is T {
return !_.isEmpty(value);
}
This SO seems like a valid use case
I think this SO question also wants this feature.
For what it’s worth, you can always work around this by making your type-guard even more fine-grained: if you use value is TheClass & { someOtherValueYouChecked: 'foobar' }, then false results will just mean it’s either not a member of TheClass or else it is but someOtherValueYouChecked wasn’t foobar—which is exactly correct.
The shortcoming is when the other values you check aren’t things you can indicate in the type domain. Even there, though, you can use “type brands,” “type tags,” or whatever you want to call them to get a nominal type to indicate this—the brand means nothing in the positive case, but in the negative case it indicates, again, that the argument is not necessarily _not_ the class in question, but rather not the intersection of that and the brand.
One-sided type-guards might still be convenient—it’s not always trivial to indicate the real type, and producing a brand type just for this is annoying. But they don’t actually make things more type-safe. I have eliminated all of the cases in our code that were looking for one-sided type-guards using these approaches.
@krryan That's awesome!!
I ran into this issue with Array.isArray... I'm sure there's another issue somewhere related to this specifically, but the current arr is any[] return type would benefit from a definition with one of these syntaxes.
A syntax like
declare interface ArrayConstructor {
isArray(arr: any): arr is readonly unknown[] else not any[];
}
while a bit awkward, would solve the problem below, wouldn't it?
With arr is any[]:
Currently the true branch loses all type safety for elements of an array even if the element types were previously known (and makes a readonly array writable, doesn't it?)
Having the guard return arr is unknown[] breaks narrowing in the false branch for a union of SomeArray | SomeObject.
I actually assigned Array.isArray to another export and redefined its typings with some overloads
to get better types out of the true branch (the more common use case), and I only use Array.isArray() when I want to assert the false branch - not an array of any type. But that still requires some care on my part that a more expressive type guard could help avoid.
//Narrows unions to those that are of array types (not 100% sure this is correct, but it's the intent).
type _ArrayCompatibleTypes<T> = T extends readonly any[] ? T : never;
// If<Pred, Then, Else> and IsNever<T> are some utility types that do what they sound like.
type ArrayCompatibleTypes<T> = If<IsNever<_ArrayCompatibleTypes<T>>, T & readonly unknown[], _ArrayCompatibleTypes<T>>;
function isArray<T extends ArrayCompatibleTypes<any>>(obj: T): obj is ArrayCompatibleTypes<T>;
function isArray<TItem>(obj: Iterable<TItem> | null | undefined): obj is TItem[];
function isArray(obj: any): obj is unknown[];
I've found several scenarios in coding where I have roughly this pattern:
class BaseClass {
type: SomeEnum;
}
class ChildClass extends BaseClass {
isCurrentlyActionable: boolean;
takeAction() {
doSomething();
}
}
function isChildClass(item: BaseClass): item is ChildClass {
return item.type === SomeEnum.ChildType;
}
function canTakeAction(item: BaseClass): boolean {
if (!isChildClass(item)) {
return false;
}
return item.isCurrentlyActionable;
}
Now, there are a number of places where I need to call canTakeAction on some BaseClass item where I do not yet know the type. I find myself littering the code with this awkwardness, comment and all:
// Putting the redundant isChildClass() check only to satisfy TypeScript
if (!canTakeAction(item) || !isChildClass(item)) {
return;
}
// Now start using item like it's a ChildClass, such as:
item.takeAction();
One alternative to the redundant check is I can just cast the item after the if block. Not really much better than the typecheck above.
Another alternative is to naively set the return type of canTakeAction to be item is ChildClass. That works well for this exact scenario, but I'll be in bad shape when I get to the scenario:
const childClass: ChildClass = new ChildClass(...);
if (canTakeAction(childClass)) {
...
} else {
// childClass is now of type never :(
}
So, for now, we just litter the code with the redundant checks. I actually haven't found myself needing the negative part of the type guard scenario as far as I can remember. I just need the positive side.
We'd also benefit from either weak type guards or one-sided guards. Our type guard library has this exact issue where if you extend the builtin types with validators, you either lose type information or it becomes unsafe.
This is safe:
const Message = struct({ from: string, to: string, date: is(Date), content: string });
declare const x: any;
if (Message(x)) {
// x is { from: string, to: string, date: Date, content: string }
} else {
// x is any
}
This is unsafe:
const Positive = refinement(number, x => x > 0);
declare const x: number | string;
if (Positive(x)) {
// if we preserved guards, x is number
// otherwise we lose validated type information
} else {
// if we preserved guards, x would be string, which is very wrong
}
With one-sided guards, we'd be able to un-guard the else branch so runtype would be safe to use with custom validators.
I am finding myself in a situation that I think is related to this issue. I would like to be able to specify the type when the condition in the guard function is not met:
type Item = {
className: 'Item';
id: string;
}
type Collection = {
className: 'Collection';
id: string;
}
type CollectionFragment = {
root: Item | Collection;
name: string;
children: string[];
// Some other properties..
}
// We would like this to be a guard, such that when the condition is met:
// item is Item | Collection
// When it is unmet:
// item is just Item
function isRoot<T extends Item | Collection>(fragment: CollectionFragment, item: T): boolean {
return fragment.root.id === item.id;
}
const item: Item | Collection;
if (isRoot(item)) {
// item should be Item | Collection
} else {
// item is definitly _not_ Collection.
// item should be Item
}
Is there a workaround for making this possible? I can do this with a isNotRoot function instead, but this isn't ideal. 🤔
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
}
}
Most helpful comment
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
elsesyntax in the examples below, but I'm notsuggesting that should be the final syntax.
New ES2015+ predicate functions
The predicate functions added in ES2015 as static methods of the
Numberconstructor 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
Objectconstructor initially introduces in ES5. TypeScriptcurrently 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.