Home > Net >  Typescript - optional type predicate
Typescript - optional type predicate

Time:07-15

I'm interested in building a type predicate function like this, which will return true when the number is a real number and simultaneously make the type stricter:

function isRealNumber(input: number | undefined | null): input is number {
    return input !== undefined && input !== null && Number.isFinite(input);
}

However, this produces incorrect types in some circumstances when negated, e.g.:

const myNumber: number | null = NaN as any;
if (isRealNumber(myNumber)) {
    const b = myNumber;  // b is number, correct
} else {
    const b = myNumber;  // b is `null`, should be `null | number`
}

This can be worked around using multiple statements in the conditional, but this is less ideal:

function isRealNumber(input: number | undefined | null): boolean {
    return input !== undefined && input !== null && Number.isFinite(input);
}

const myNumber: number | null = NaN as any;
if (isRealNumber(myNumber) && myNumber !== null && myNumber !== undefined) {
    const b = myNumber;  // b is number, correct
} else {
    const b = myNumber;  // b is `null | number`, correct
}

Is there any way in typescript to have a single function which will correctly narrow the type without also producing incorrect types sometimes when negated?

CodePudding user response:

You're looking for a "one-sided" or "fine-grained" type predicate as requested in microsoft/TypeScript#15048, but this is not currently directly supported in TypeScript. The issue is open, but it's not clear that anything will happen here.

There is a workaround mentioned in that issue, though. If you change your guarded type from input is number to input is RealNumber where RealNumber is some type assignable to but strictly narrower than number, then things will start working.

And while there is no actual distinction in the type system between RealNumber and number, you can "fake" one by using a technique called branded primitives. That's where you take a primitive type like number and intersect it with an object type containing a phantom "brand" or "tag" property. For example:

function isRealNumber(input: number | undefined | null): input is number & { __realNumber?: true } {
    return input !== undefined && input !== null && Number.isFinite(input);
}

const myNumber: number | null = NaN as any;
if (isRealNumber(myNumber)) {
    const b = myNumber;  // b is number & {__realNumber?: true}
} else {
    const b = myNumber;  // b is number | null
}

That looks more reasonable. If isRealNumber(myNumber) is true, then b is narrowed to number & {__realNumber?: true}. That {__realNumber?: true} property isn't going to actually be there at runtime (it's a "phantom" property), but that shouldn't matter much, since b is still a number. And when isRealNumber(myNumber) is false, then b is not narrowed at all.

Playground link to code

  • Related