Type guard does not narrow union type correctly in TypeScript

Type guard does not narrow union type correctly in TypeScript
typescript
Ethan Jackson

I'm encountering an issue where a custom type guard doesn't properly narrow down a union type.

Here's a simplified example:

type Board = { type: string; }; type Boards = { [boardNumber: number]: string; }; function isBoard(value: unknown): value is Board { return true; } function isBoards(value: unknown): value is Boards { return true; } const process = (xxx: Boards | Board | string | number) => { if (isBoards(xxx)) { // Problem: TypeScript infers xxx as string | Board | Boards // I expected xxx to be narrowed to Boards console.log(xxx); } if (isBoard(xxx)) { // Here, xxx is correctly narrowed to Board console.log(xxx); } };

Playground

Why does the isBoards type guard fail to narrow the union properly, while isBoard works as expected?

Is there something I'm missing about how TypeScript applies type guards in unions involving index signatures or object types?

Answer

In TypeScript, a plain string actually has a built-in numeric index signature, which means you can index into a string to get a substring of type string. For example this code is acceptable:

let value: Boards = "myString";

To solve this problem add a dummy type to Boards:

type Boards = { [boardNumber: number]: string; __dummy_prop__?: unknown; };

Related Articles