'How can I make a type predicate that only narrows if it returns true?
For example, an isLongString function that returns true if and only if the argument is a string with more than 10 characters couldn't be (naively) implemented as a type predicate because it might confuse the compiler into narrowing incorrectly.
function isLongString(v: unknown): v is string {
return typeof v === "string" && v.length > 10;
}
const shortString = "short" as string | number;
if (isLongString(shortString)) {
const tst: string = shortString;
// @ts-expect-error
const tst2: number = shortString;
} else {
// @ts-expect-error
const tst: string = shortString;
// @ts-expect-error
const tst2: number = shortString;
}
Solution 1:[1]
Typescript's type system isn't able to express types like "a string of length at least 11", although if you wanted "a string of length exactly 11" then you could write that like string & {length: 11}. If you have a type like this, which is an intersection of two types, then narrowing will (typically) be one-way in the sense that no narrowing is done in the else branch, because it isn't known which of the two intersected types was not satisfied. (Narrowing would still occur in a union like (string & {length: 11}) | number, of course.)
In the specific case of strings, you may be able to solve your problem by writing a suitable template literal type which your type guard checks for. For example:
function isStringStartingWithFoo(s: unknown): s is `foo${string}` {
return typeof s === 'string' && s.startsWith('foo');
}
Unfortunately there is no (sane) way to use a template literal type to describe "a string of length at least 11", but if your function is testing something more concrete than that, then a template literal type may be the way forward.
In the general case, the solution is to write a return type like v is string & ... with an intersection type. The ... part could be something fictitious, as in @johncs's answer. Using fictitious properties to emulate nominal types is quite standard in Typescript, and shouldn't lead to incorrect code because nobody will have any reason to try to access the non-existent property. However, you could still instead write something true which is related to the fact that the string has a length of at least 11.
For example, you could say that the type guard checks that it's a string whose match method doesn't return null when called with the regex .{11,}:
type LongString = string & {match(regex: '.{11,}'): RegExpMatchArray}
function isLongString(v: unknown): v is LongString {
// ...
}
(Note that this doesn't prevent calling match with other arguments, because intersection types of functions or methods are like overloads.) This is a bit clumsy in this example because you are unlikely to ever want to call .match('.{11,}') exactly on a string anyway, but one can imagine examples where it is actually useful, perhaps a type guard that checks v is MyDataStructure & {isEmpty(): false} or something similar.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
