Type Narrowing in TypeScript is the process where the compiler understands that in a specific code block, a variable undoubtedly has a more specific type based on certain conditions.
Typical narrowing techniques include:
typeof operator:function example(x: number | string) { if (typeof x === 'string') { // x: string here return x.toUpperCase(); } else { // x: number here return x.toFixed(2); } }
instanceof (for classes):if (dateObj instanceof Date) { // dateObj: Date }
null and undefined:function print(value?: string) { if (value != null) { // value: string console.log(value.length); } }
type Pet = { kind: 'dog'; woof: () => void } | { kind: 'cat'; meow: () => void }; function sound(pet: Pet) { if (pet.kind === 'dog') { pet.woof(); } else { pet.meow(); } }
TypeScript also supports user-defined type guard functions:
function isString(x: unknown): x is string { return typeof x === 'string'; }
Narrowing makes type checking safer and the code more reliable.
Can you guarantee type narrowing through normal comparisons (e.g.,
==/===), and does it always work?
Answer: No. TypeScript does not understand the type from simple comparisons in all cases, especially if the comparison is too "vague" or involves indirect variables/properties. Narrowing often requires explicit mechanisms (typeof, instanceof, discriminant properties, and type guards).
Example:
function foo(x: number | string | null) { if (x) { // x: string | number, null is no longer possible, but narrowing to a specific type will not occur } }
History
In a large TypeScript project, the condition user.role == 'admin' did not narrow the data type, and there still needed to be checks for property existence. Developers underestimated the narrowing rules, leading to "Cannot read property ... of undefined" errors.
History
In a mobile application, a function accepted either an object or a string. Due to an indirect function call that changed the type, narrowing did not occur, and there was a crash on some devices when calling a method that was absent on the string. Tests failed on rare cases.
History
When migrating code from JavaScript to TypeScript, user-defined type guard functions were not implemented, assuming that property checks would always narrow the type. As a result, complex objects with optional fields behaved incorrectly, leading to unpredictable runtime data access errors.