Home > Enterprise >  Typescript type guard union type
Typescript type guard union type

Time:04-14

I have the following type guard:

enum X {
    A = "1"
}

const isNullableX = (value: any): value is X | null => false

let foo: string | null = ''

if (isNullableX(foo)) {
  console.log(foo)
}

I would expect the type of foo in the last console.log to be X | null but instead, it's only X. Can someone explain to me how this works? If I change the declaration of foo to let foo: unknown = '' then the type in the console.log is inferred correctly.

Here's an example to play with: Playground

CodePudding user response:

TypeScript uses control flow analysis to narrow the apparent type of an expression to be more specific. For example, when you assign a value to a variable of a union type, it will narrow the type of the variable to just those union members which work for that value, at least until the next assignment. So for this:

let foo: string | null = ''    
foo // string

The compiler has already narrowed foo from string | null to just string. The compiler knows that foo is not null after that assignment. (If you change this to let foo = Math.random()<0.5 ? '' : null then the assignment won't narrow and things might behave as you expect later.)


Control flow analysis will either narrow the apparent type of an expression, or reset the type back to the annotated or original type. What you can't realistically do is arbitrarily widen a type, or mutate a type to something unrelated.

When you call a user-defined type guard function like

const isNullableX = (value: any): value is X | null => false

it will, depending on the output, narrow the type of its input. In your call here:

if (isNullableX(foo)) {
  foo // X
}

you are narrowing foo from string to something assignable to X | null. The only plausible result here is X, since null is not assignable to string. And so foo is narrowed to X. If you were expecting foo to change from string to X | null, that can't happen because it's sort of an arbitrary mutation (I suppose it could be a reset-followed-by-a-narrowing, but there's no resetting because there's no reassignment).

Playground link to code

CodePudding user response:

How about this...

enum X {
    A = "1"
}

function assertIsTrue(condition: unknown): asserts condition is boolean {
  if (!condition) {
    throw new Error("Not True!");
  }
}

const isNullableX = (value: any): value is X | null => false

let foo = '' as string | null;

assertIsTrue(isNullableX(foo));

console.log(foo);
  • Related