Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - optional type predicate

Tags:

typescript

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?

like image 246
Daniel Centore Avatar asked Sep 03 '26 09:09

Daniel Centore


1 Answers

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

like image 185
jcalz Avatar answered Sep 07 '26 17:09

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!