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);
}
};
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;
};