Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to infer type for calculated logic

Tags:

typescript

I have an interface with field1 having multiple possible data types: number, object, or boolean.

I have if-condition to check the data type is number before processing the data.

For case1 and case2, TypeScript correctly identify field1 is now a number type, but not on case3 which is the problem I try to solve in my project.

How to get TypeScript knows that, in case3, field1 is now a number type based on isCorrectNumber result?

I need to use the isCorrectNumber in various places

interface Module1 {
  field1: number | {key: string; value: string}[] | boolean;
}

const value: Module1 = {
  field1: [{key: 'key', value: 'value'}]
};

if (typeof value.field1 === 'number' && value.field1 % 2 === 0 && (value.field1 > 0 || value.field1 < 100)) {
  const myString: number = value.field1; // Case1: WORKS
  console.log(myString);
}

const isCorrectNumberFn = (value: Module1): value is {field1: number} => (typeof value.field1 === 'number' && value.field1 % 2 === 0&& (value.field1 > 0 || value.field1 < 100));
if (isCorrectNumberFn(value)) {
  const myString: number = value.field1; // Case2: WORKS
  console.log(myString);
}

const isCorrectNumber = typeof value.field1 === 'number' && value.field1 % 2 === 0&& (value.field1 > 0 || value.field1 < 100);
if (isCorrectNumber) {
  const myString: number = value.field1; // Case3: ERROR: Type 'number | boolean | { key: string; value: string; }[]' is not assignable to type 'number'. Type 'boolean' is not assignable to type 'number'
  console.log(myString);
}

TypeScript playground

like image 320
aha Avatar asked Nov 18 '25 15:11

aha


1 Answers

TypeScript's ability to narrow the apparent types of values based on type guards is limited to very specific scenarios that were explicitly implemented as such. TypeScript isn't an arbitrary proof solver capable of computing all possible future states of a program. That would be prohibitively expensive in terms of compiler performance. There will always be situations in which a clever human could say "I know that x must be of type Y at this point in the program" but where TypeScript cannot recognize that. See microsoft/TypeScript#52822 for example.

Again, the situations in which TypeScript can narrow types are limited to those situations where someone explicitly programmed the compiler to do so. And these are situations where the added complexity and performance hit of making the compiler check whether such a narrowing is possible is paid for by the overall improvement in developer experience.

So, for example, direct typeof type guards are supported:

if (typeof value.field1 === 'number' && ⋯) {
  const n: number = value.field1; // okay
}

And, to handle situations where the compiler does not type guard as desired automatically, the language allows users to write their own custom type guard functions:

const isCorrectNumberFn = (value: Module1): value is {field1: number} => (
  typeof value.field1 === 'number' && ⋯);

if (isCorrectNumberFn(value)) {
  const n: number = value.field1; // okay
}

But of course, not all scenarios are supported. You ran into one unsupported situation:

const isCorrectNumber =
  typeof value.field1 === 'number' && ⋯;
if (isCorrectNumber) {
  const n: number = value.field1; // error!    
}

Before TypeScript 4.4, this was unsupported because it was simply impossible to "save" the results of a type guard into another variable and use it for narrowing. But TypeScript 4.4 introduced support for narrowing based on aliased conditions saved into boolean variables, as implemented in microsoft/TypeScript#44730.

So why then doesn't your code above work? It's because "narrowing through indirect references occurs only when [...] the reference being narrowed is a const variable, a readonly property, or a parameter for which there are no assignments in the function body." You are trying to narrow value.field1, which is a mutable property of Module1, and not a readonly property. The compiler doesn't bother trying to hold onto the results of isCorrectNumber for later control flow analysis, because it's possible that the value you checked, value.field1, might be reassigned in the interim. The compiler doesn't want to spend extra time checking to make sure that this has not happened. It just gives up.


That leads directly to a possible alternative approach: Make the field1 property readonly so that the compiler knows it's relatively safe to use as a type guard even without checking for reassignment:

interface Module1 {
  readonly field1: (
    number | { key: string; value: string }[] | boolean
  );
}

And then this will suddenly work:

const isCorrectNumber =
  typeof value.field1 === 'number' && ⋯;
if (isCorrectNumber) {
  const n: number = value.field1; // okay
}

Such an approach might not always be appropriate, but it's the best the compiler can currently do. If you cannot treat field1 as readonly, then perhaps it's better not to try to hold onto checks for its type in the first place. You could refactor to use a direct check, or to use a custom type guard function where you're responsible for maintaining correctness.

Playground link to code

like image 126
jcalz Avatar answered Nov 21 '25 11:11

jcalz