Skip to main content
Practice Problems

Type narrowing in TypeScript

What is Type Narrowing?

Type Narrowing is the process by which TypeScript refines a variable's type from a more general to a more specific one based on checks in the code.

typescript
function process(value: string | number) { // Here value: string | number if (typeof value === 'string') { // Here value: string (type narrowed!) console.log(value.toUpperCase()); } else { // Here value: number (only number remains) console.log(value.toFixed(2)); } }

Ways to Narrow Types

typeof Guard

Checking primitive types via typeof.

typescript
function printValue(value: string | number | boolean) { if (typeof value === 'string') { console.log(value.toUpperCase()); } else if (typeof value === 'number') { console.log(value.toFixed(2)); } else { console.log(value ? 'true' : 'false'); } }

Important:

typeof null returns 'object', this is a JavaScript feature!

typescript
function process(value: string | null) { if (typeof value === 'object') { // Here value is still string | null (null is object!) console.log(value); // can be null } }

instanceof Guard

Checking class membership.

typescript
class Dog { bark() { console.log('Woof!'); } } class Cat { meow() { console.log('Meow!'); } } function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); // animal: Dog } else { animal.meow(); // animal: Cat } }

Working with Built-in Classes

typescript
function processValue(value: Date | string) { if (value instanceof Date) { console.log(value.getFullYear()); // value: Date } else { console.log(value.toUpperCase()); // value: string } }

in Operator

Checking property presence in object.

typescript
interface Circle { radius: number; } interface Square { size: number; } type Shape = Circle | Square; function getArea(shape: Shape) { if ('radius' in shape) { // shape: Circle return Math.PI * shape.radius ** 2; } else { // shape: Square return shape.size ** 2; } }

Checking Methods

typescript
interface Bird { fly(): void; layEggs(): void; } interface Fish { swim(): void; layEggs(): void; } function move(animal: Bird | Fish) { if ('fly' in animal) { animal.fly(); // animal: Bird } else { animal.swim(); // animal: Fish } }

Equality Narrowing

Narrowing through equality check.

typescript
function process(x: string | number, y: string | boolean) { if (x === y) { // x and y can only be equal if both are string console.log(x.toUpperCase()); // x: string console.log(y.toUpperCase()); // y: string } }

Checking for null and undefined

typescript
function printName(name: string | null | undefined) { if (name !== null && name !== undefined) { console.log(name.toUpperCase()); // name: string } // Or shorter if (name != null) { console.log(name.toUpperCase()); // name: string } }

Truthiness Narrowing

Narrowing based on truthy/falsy check.

typescript
function printLength(str: string | null | undefined) { if (str) { // str: string (removed null and undefined) console.log(str.length); } }

Falsy Values

typescript
function process(value: string | number | null | undefined | 0 | '') { if (value) { // value: string | number (removed falsy values) // BUT! 0 and '' are also falsy, so they're removed too } }

Caution:

Truthiness narrowing removes ALL falsy values: 0, '', false, null, undefined, NaN.

More Precise Check

typescript
function processValue(value: string | null) { if (value !== null) { // value: string console.log(value.length); } }

Type Predicates (is)

Custom type guards with is keyword.

typescript
function isString(value: unknown): value is string { return typeof value === 'string'; } function process(value: unknown) { if (isString(value)) { // value: string console.log(value.toUpperCase()); } }

More Complex Checks

typescript
interface User { name: string; email: string; } function isUser(obj: unknown): obj is User { return ( typeof obj === 'object' && obj !== null && 'name' in obj && 'email' in obj && typeof (obj as User).name === 'string' && typeof (obj as User).email === 'string' ); } function greetUser(data: unknown) { if (isUser(data)) { // data: User console.log(`Hello, ${data.name}!`); } }

Discriminated Unions

Narrowing based on common discriminating property.

typescript
type Success = { status: 'success'; data: string; }; type Error = { status: 'error'; message: string; }; type Result = Success | Error; function handleResult(result: Result) { if (result.status === 'success') { // result: Success console.log(result.data); } else { // result: Error console.log(result.message); } }

Switch Statement

typescript
type Action = | { type: 'INCREMENT' } | { type: 'DECREMENT' } | { type: 'SET'; value: number }; function reducer(state: number, action: Action): number { switch (action.type) { case 'INCREMENT': // action: { type: 'INCREMENT' } return state + 1; case 'DECREMENT': // action: { type: 'DECREMENT' } return state - 1; case 'SET': // action: { type: 'SET'; value: number } return action.value; } }

Assignment Narrowing

Narrowing on assignment.

typescript
let value: string | number; value = 'hello'; // value: string (narrowed to string) console.log(value.toUpperCase()); value = 42; // value: number (narrowed to number) console.log(value.toFixed(2));

Control Flow Analysis

TypeScript analyzes code execution flow.

typescript
function process(value: string | null) { if (value === null) { return; } // value: string (null excluded after return) console.log(value.toUpperCase()); }

Throw Statements

typescript
function assertIsString(value: unknown): asserts value is string { if (typeof value !== 'string') { throw new Error('Not a string!'); } } function process(value: unknown) { assertIsString(value); // value: string (after assertion) console.log(value.toUpperCase()); }

Practical Examples

Handling API Response

typescript
type ApiResponse<T> = | { success: true; data: T } | { success: false; error: string }; async function fetchUser(id: number): Promise<ApiResponse<User>> { // ... } const response = await fetchUser(1); if (response.success) { // response: { success: true; data: User } console.log(response.data.name); } else { // response: { success: false; error: string } console.error(response.error); }

Validating Form Fields

typescript
interface FormData { name?: string; email?: string; age?: number; } function validateForm(data: FormData): boolean { if (!data.name) { console.error('Name is required'); return false; } // data.name: string (not undefined) if (data.name.length < 3) { console.error('Name too short'); return false; } if (!data.email) { console.error('Email is required'); return false; } // data.email: string if (!data.email.includes('@')) { console.error('Invalid email'); return false; } return true; }

Working with Events

typescript
function handleEvent(event: MouseEvent | KeyboardEvent) { if (event instanceof MouseEvent) { console.log(`Mouse: ${event.clientX}, ${event.clientY}`); } else { console.log(`Key: ${event.key}`); } }

Array.isArray()

typescript
function process(value: string | string[]) { if (Array.isArray(value)) { // value: string[] value.forEach(item => console.log(item)); } else { // value: string console.log(value); } }

Never Type and Exhaustiveness

typescript
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; size: number }; function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': return Math.PI * shape.radius ** 2; case 'square': return shape.size ** 2; default: // shape: never (all cases handled) const _exhaustive: never = shape; return _exhaustive; } }

If we add a new type, TypeScript will error:

typescript
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; size: number } | { kind: 'triangle'; base: number; height: number }; // Error in default case!

Type Narrowing Limitations

Variable Changes in Callbacks

typescript
function process(value: string | null) { if (value !== null) { setTimeout(() => { // Error! value might have changed console.log(value.toUpperCase()); }, 1000); } value = null; // Changed! }

Object Mutations

typescript
interface Container { value: string | number; } function process(container: Container) { if (typeof container.value === 'string') { setTimeout(() => { // Error! value could have changed console.log(container.value.toUpperCase()); }, 0); } }

Best Practices

Use Type Guards for Complex Checks

typescript
// Bad function process(data: unknown) { if ( typeof data === 'object' && data !== null && 'name' in data && typeof (data as any).name === 'string' ) { console.log((data as { name: string }).name); } } // Good function isUser(data: unknown): data is { name: string } { return ( typeof data === 'object' && data !== null && 'name' in data && typeof (data as any).name === 'string' ); } function process(data: unknown) { if (isUser(data)) { console.log(data.name); } }

Early Return for Simplification

typescript
// Bad function process(value: string | null) { if (value !== null) { console.log(value.toUpperCase()); console.log(value.length); // lots of code... } } // Good function process(value: string | null) { if (value === null) return; // value: string throughout function console.log(value.toUpperCase()); console.log(value.length); // lots of code... }

Use Discriminated Unions

typescript
// Bad interface Result { success: boolean; data?: string; error?: string; } // Good type Result = | { success: true; data: string } | { success: false; error: string };

Conclusion

Type Narrowing:

  • Automatic type refinement based on checks
  • typeof for primitives
  • instanceof for classes
  • in for object properties
  • Equality and truthiness narrowing
  • Type predicates (is) for custom checks
  • Discriminated unions for states
  • Control flow analysis tracks execution flow
  • Never for exhaustiveness checking

In Interviews:

Important to be able to:

  • Explain type narrowing concept
  • Show different narrowing methods (typeof, instanceof, in)
  • Write type guard with is
  • Explain discriminated unions
  • Show exhaustiveness checking via never
  • Discuss limitations (callbacks, mutations)

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems