Built-in Type Guards
Built-in Type Guards
typeof
function processValue(value: string | number | boolean): string {
if (typeof value === 'string') {
return value.toUpperCase(); // TypeScript knows value is string
}
if (typeof value === 'number') {
return value.toFixed(2); // TypeScript knows value is number
}
return value ? 'true' : 'false'; // TypeScript knows value is boolean
}
instanceof
class HttpError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
}
}
function handleError(error: Error): string {
if (error instanceof HttpError) {
return `HTTP ${error.statusCode}: ${error.message}`;
}
if (error instanceof ValidationError) {
return `Validation failed on ${error.field}: ${error.message}`;
}
return error.message;
}
in Operator
interface Circle { kind: 'circle'; radius: number }
interface Square { kind: 'square'; side: number }
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if ('radius' in shape) {
return Math.PI * shape.radius ** 2;
}
return shape.side ** 2;
}
Custom Type Guards
Custom Type Guards
Type Predicate Functions
interface Cat { type: 'cat'; meow(): void }
interface Dog { type: 'dog'; bark(): void }
type Pet = Cat | Dog;
function isCat(pet: Pet): pet is Cat {
return pet.type === 'cat';
}
function isDog(pet: Pet): pet is Dog {
return pet.type === 'dog';
}
function handlePet(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // TypeScript knows pet is Cat
} else {
pet.bark(); // TypeScript knows pet is Dog
}
}
Real-World Example: API Response
interface SuccessResponse {
ok: true;
data: unknown;
}
interface ErrorResponse {
ok: false;
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(response: ApiResponse): response is SuccessResponse {
return response.ok === true;
}
function handleResponse(response: ApiResponse): void {
if (isSuccess(response)) {
console.log(response.data); // safe access
} else {
console.error(response.error); // safe access
}
}
Checking for Property Existence
function processInput(input: unknown): string {
if (
typeof input === 'object' &&
input !== null &&
'name' in input &&
typeof (input as any).name === 'string'
) {
return (input as { name: string }).name;
}
return 'unknown';
}
Assertion Functions
Assertion Functions
assertNever
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}
type Shape = 'circle' | 'square' | 'triangle';
function getArea(shape: Shape): number {
switch (shape) {
case 'circle': return Math.PI * 100;
case 'square': return 100;
case 'triangle': return 50;
default: return assertNever(shape);
}
}
Assertion Functions
function assertString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined');
}
}
// Usage
function processName(input: unknown): string {
assertString(input); // narrows to string
return input.toUpperCase(); // safe
}
function getLength(value: string | null): number {
assertDefined(value); // narrows to string
return value.length; // safe
}
Exhaustive Check Pattern
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
function handleMethod(method: HttpMethod): string {
switch (method) {
case 'GET': return 'Read';
case 'POST': return 'Create';
case 'PUT': return 'Update';
case 'DELETE': return 'Remove';
default: return assertNever(method);
}
}
Control Flow Analysis
Control Flow Analysis
Smart Casting in Conditions
function process(data: string | number | null): string {
// TypeScript narrows based on control flow
if (data === null) return 'empty';
// data is string | number here
if (typeof data === 'string') {
// data is string here
return data.toUpperCase();
}
// data is number here
return data.toString();
}
Truthiness Narrowing
function trimAndUpper(value: string | null | undefined): string {
// null and undefined are falsy
if (!value) return '';
// value is string here
return value.trim().toUpperCase();
}
Discriminated Unions as Guards
interface Loading { status: 'loading' }
interface Success { status: 'success'; data: string }
interface Error { status: 'error'; message: string }
type State = Loading | Success | Error;
function render(state: State): string {
switch (state.status) {
case 'loading': return 'Loading...';
case 'success': return state.data; // narrowed
case 'error': return state.message; // narrowed
}
}
Best Practices
- Prefer discriminated unions over type guards for closed unions
- Use type predicates for runtime checks on external data
- Use
assertsfor validation functions - Always handle the
nevercase for exhaustive checks