Table of Contents
- Introduction
- Basic Type Checking
- Built-in Type Guards
- Custom Type Guards
- Utility Types for Type Checking
- Advanced Type Checking Techniques
- Runtime Type Checking
- Type Narrowing Strategies
- Generic Type Checking
- Union and Intersection Type Checking
- Object and Array Type Checking
- Function Type Checking
- Class and Interface Type Checking
- Error Handling and Type Safety
- Best Practices and Performance
Introduction
TypeScript provides powerful type checking capabilities that help catch errors at compile time and improve code reliability. This guide covers every aspect of type checking in TypeScript, from basic techniques to advanced patterns.
Basic Type Checking
Primitive Type Checking
// Basic typeof checks
function checkPrimitiveTypes(value: unknown): string {
if (typeof value === 'string') {
return `String: ${value.toUpperCase()}`; // value is narrowed to string
}
if (typeof value === 'number') {
return `Number: ${value.toFixed(2)}`; // value is narrowed to number
}
if (typeof value === 'boolean') {
return `Boolean: ${value ? 'true' : 'false'}`; // value is narrowed to boolean
}
if (typeof value === 'bigint') {
return `BigInt: ${value.toString()}`; // value is narrowed to bigint
}
if (typeof value === 'symbol') {
return `Symbol: ${value.toString()}`; // value is narrowed to symbol
}
return 'Unknown type';
}
// Usage examples
console.log(checkPrimitiveTypes("hello")); // String: HELLO
console.log(checkPrimitiveTypes(42)); // Number: 42.00
console.log(checkPrimitiveTypes(true)); // Boolean: true
Null and Undefined Checking
// Checking for null and undefined
function handleNullish(value: string | null | undefined): string {
// Method 1: Explicit checks
if (value === null) {
return 'Value is null';
}
if (value === undefined) {
return 'Value is undefined';
}
// Method 2: Nullish coalescing
const result = value ?? 'Default value';
// Method 3: Combined nullish check
if (value == null) { // checks both null and undefined
return 'Value is nullish';
}
return value; // value is narrowed to string
}
// Non-null assertion operator (use with caution)
function useNonNullAssertion(value: string | null) {
const definitelyString = value!; // Asserts value is not null
return definitelyString.toUpperCase();
}
Built-in Type Guards
Array.isArray()
function processArrayOrSingle(value: string | string[]): string[] {
if (Array.isArray(value)) {
return value; // value is narrowed to string[]
}
return [value]; // value is narrowed to string
}
// Advanced array type checking
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function processUnknownArray(value: unknown): string[] {
if (isStringArray(value)) {
return value; // value is narrowed to string[]
}
throw new Error('Not a string array');
}
instanceof Operator
class User {
constructor(public name: string) {}
greet() { return `Hello, ${this.name}`; }
}
class Admin extends User {
constructor(name: string, public permissions: string[]) {
super(name);
}
hasPermission(perm: string) {
return this.permissions.includes(perm);
}
}
function handleUserType(user: User | Admin | string): string {
if (user instanceof Admin) {
return `Admin: ${user.name}, Permissions: ${user.permissions.join(', ')}`;
}
if (user instanceof User) {
return `User: ${user.greet()}`;
}
if (typeof user === 'string') {
return `String: ${user}`;
}
return 'Unknown type';
}
// Error handling with instanceof
function safeInstanceCheck(obj: unknown): string {
try {
if (obj instanceof Date) {
return obj.toISOString();
}
if (obj instanceof RegExp) {
return obj.source;
}
if (obj instanceof Error) {
return obj.message;
}
return 'Not a recognized instance';
} catch (error) {
return 'Error during instance check';
}
}
Custom Type Guards
Basic Type Guards
// Simple type guard functions
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
// Object type guards
interface Person {
name: string;
age: number;
}
function isPerson(obj: unknown): obj is Person {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'age' in obj &&
typeof (obj as Person).name === 'string' &&
typeof (obj as Person).age === 'number'
);
}
// Usage
function processPerson(data: unknown): string {
if (isPerson(data)) {
return `${data.name} is ${data.age} years old`; // data is narrowed to Person
}
throw new Error('Invalid person data');
}
Advanced Type Guards
// Generic type guard
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(guard);
}
// Union type guards
type Status = 'pending' | 'completed' | 'failed';
function isStatus(value: unknown): value is Status {
return typeof value === 'string' &&
['pending', 'completed', 'failed'].includes(value);
}
// Complex object type guard
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
function isApiResponse<T>(
obj: unknown,
dataGuard: (data: unknown) => data is T
): obj is ApiResponse<T> {
return (
typeof obj === 'object' &&
obj !== null &&
'data' in obj &&
'status' in obj &&
'message' in obj &&
typeof (obj as any).status === 'number' &&
typeof (obj as any).message === 'string' &&
dataGuard((obj as any).data)
);
}
// Discriminated union type guard
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
type Shape = Circle | Square;
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle';
}
function isSquare(shape: Shape): shape is Square {
return shape.kind === 'square';
}
function calculateArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2;
}
if (isSquare(shape)) {
return shape.size ** 2;
}
// TypeScript knows this is unreachable
const _exhaustive: never = shape;
throw new Error('Unhandled shape type');
}
Utility Types for Type Checking
Built-in Utility Types
// Partial type checking
interface Config {
host: string;
port: number;
secure: boolean;
}
function isPartialConfig(obj: unknown): obj is Partial<Config> {
if (typeof obj !== 'object' || obj === null) {
return false;
}
const config = obj as Partial<Config>;
return (
(config.host === undefined || typeof config.host === 'string') &&
(config.port === undefined || typeof config.port === 'number') &&
(config.secure === undefined || typeof config.secure === 'boolean')
);
}
// Required type checking
function isRequiredConfig(obj: unknown): obj is Required<Config> {
return (
typeof obj === 'object' &&
obj !== null &&
'host' in obj &&
'port' in obj &&
'secure' in obj &&
typeof (obj as Config).host === 'string' &&
typeof (obj as Config).port === 'number' &&
typeof (obj as Config).secure === 'boolean'
);
}
// Pick type checking
type ConfigCredentials = Pick<Config, 'host' | 'port'>;
function isConfigCredentials(obj: unknown): obj is ConfigCredentials {
return (
typeof obj === 'object' &&
obj !== null &&
'host' in obj &&
'port' in obj &&
typeof (obj as ConfigCredentials).host === 'string' &&
typeof (obj as ConfigCredentials).port === 'number'
);
}
// Omit type checking
type PublicConfig = Omit<Config, 'secure'>;
function isPublicConfig(obj: unknown): obj is PublicConfig {
return (
typeof obj === 'object' &&
obj !== null &&
'host' in obj &&
'port' in obj &&
!('secure' in obj) &&
typeof (obj as PublicConfig).host === 'string' &&
typeof (obj as PublicConfig).port === 'number'
);
}
Custom Utility Types
// DeepPartial utility type
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// NonNullable utility
type NonNullable<T> = T extends null | undefined ? never : T;
function isNonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
// KeyOf utility for runtime checking
function hasKey<T extends object>(
obj: T,
key: string | number | symbol
): key is keyof T {
return key in obj;
}
// Safe property access
function getProperty<T extends object, K extends keyof T>(
obj: T,
key: K
): T[K] {
if (hasKey(obj, key)) {
return obj[key];
}
throw new Error(`Property ${String(key)} not found`);
}
Advanced Type Checking Techniques
Conditional Types
// Conditional type checking
type IsArray<T> = T extends any[] ? true : false;
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type IsPromise<T> = T extends Promise<any> ? true : false;
// Runtime conditional type checking
function checkArrayType<T>(value: T): value is T extends any[] ? T : never {
return Array.isArray(value) as any;
}
function checkFunctionType<T>(value: T): value is T extends Function ? T : never {
return typeof value === 'function' as any;
}
// Extract and Exclude utilities
type StringKeys<T> = Extract<keyof T, string>;
type NonStringKeys<T> = Exclude<keyof T, string>;
interface MixedKeys {
[key: string]: any;
[key: number]: any;
symbol: symbol;
}
function getStringKeys<T extends object>(obj: T): StringKeys<T>[] {
return Object.keys(obj) as StringKeys<T>[];
}
Mapped Types
// Readonly type checking
type ReadonlyVersion<T> = {
readonly [P in keyof T]: T[P];
};
function isReadonly<T extends object>(
obj: T | ReadonlyVersion<T>
): obj is ReadonlyVersion<T> {
// This is a logical check - in runtime, readonly is not enforceable
return Object.isFrozen(obj);
}
// Mutable type checking
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
function makeMutable<T extends object>(obj: T): Mutable<T> {
return { ...obj } as Mutable<T>;
}
// Optional to Required conversion
type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
function hasRequiredKeys<T extends object, K extends keyof T>(
obj: T,
keys: K[]
): obj is RequiredKeys<T, K> {
return keys.every(key => obj[key] !== undefined);
}
Runtime Type Checking
JSON Schema Validation
// Simple JSON schema validator
interface Schema {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
properties?: Record<string, Schema>;
items?: Schema;
required?: string[];
}
function validateSchema(value: unknown, schema: Schema): boolean {
switch (schema.type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'array':
if (!Array.isArray(value)) return false;
if (schema.items) {
return value.every(item => validateSchema(item, schema.items!));
}
return true;
case 'object':
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false;
}
if (schema.properties) {
const obj = value as Record<string, unknown>;
// Check required properties
if (schema.required) {
for (const key of schema.required) {
if (!(key in obj)) return false;
}
}
// Validate properties
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (key in obj) {
if (!validateSchema(obj[key], propSchema)) {
return false;
}
}
}
}
return true;
default:
return false;
}
}
// Usage example
const userSchema: Schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
active: { type: 'boolean' }
},
required: ['name', 'age']
};
function validateUser(data: unknown): data is { name: string; age: number; active?: boolean } {
return validateSchema(data, userSchema);
}
Runtime Type Guards with Error Details
// Enhanced type checking with detailed errors
class TypeCheckError extends Error {
constructor(
public path: string,
public expected: string,
public received: string
) {
super(`Type check failed at ${path}: expected ${expected}, received ${received}`);
}
}
function assertType<T>(
value: unknown,
guard: (v: unknown) => v is T,
path = 'root'
): asserts value is T {
if (!guard(value)) {
throw new TypeCheckError(path, 'valid type', typeof value);
}
}
// Deep object validation
function validateObjectDeep(
obj: unknown,
validators: Record<string, (v: unknown) => boolean>,
path = 'root'
): void {
if (typeof obj !== 'object' || obj === null) {
throw new TypeCheckError(path, 'object', typeof obj);
}
const target = obj as Record<string, unknown>;
for (const [key, validator] of Object.entries(validators)) {
const currentPath = `${path}.${key}`;
if (!(key in target)) {
throw new TypeCheckError(currentPath, 'property to exist', 'undefined');
}
if (!validator(target[key])) {
throw new TypeCheckError(
currentPath,
'valid value',
String(target[key])
);
}
}
}
// Usage
try {
validateObjectDeep(
{ name: 'John', age: '30' }, // age should be number
{
name: (v): v is string => typeof v === 'string',
age: (v): v is number => typeof v === 'number'
}
);
} catch (error) {
if (error instanceof TypeCheckError) {
console.error(error.message); // Type check failed at root.age: expected valid value, received 30
}
}
Type Narrowing Strategies
Control Flow Analysis
// Type narrowing with control flow
function processValue(value: string | number | null): string {
// Early return pattern
if (value === null) {
return 'null value';
}
if (typeof value === 'string') {
return value.toUpperCase(); // value is narrowed to string
}
// At this point, value is narrowed to number
return value.toFixed(2);
}
// Complex narrowing with multiple conditions
function complexNarrowing(
input: string | number | boolean | null | undefined
): string {
if (input == null) { // null or undefined
return 'nullish';
}
if (typeof input === 'boolean') {
return input ? 'true' : 'false';
}
if (typeof input === 'string') {
if (input.length === 0) {
return 'empty string';
}
return input;
}
// input is now narrowed to number
return input.toString();
}
Discriminated Unions
// Tagged union types
interface LoadingState {
status: 'loading';
}
interface SuccessState {
status: 'success';
data: any;
}
interface ErrorState {
status: 'error';
error: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
function handleAsyncState(state: AsyncState): string {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
return `Success: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error}`;
default:
// Exhaustiveness checking
const _exhaustive: never = state;
throw new Error('Unhandled state');
}
}
// Pattern matching with type narrowing
function isLoadingState(state: AsyncState): state is LoadingState {
return state.status === 'loading';
}
function isSuccessState(state: AsyncState): state is SuccessState {
return state.status === 'success';
}
function isErrorState(state: AsyncState): state is ErrorState {
return state.status === 'error';
}
in Operator
// Using 'in' operator for type narrowing
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
type Animal = Bird | Fish;
function moveAnimal(animal: Animal): void {
if ('fly' in animal) {
animal.fly(); // animal is narrowed to Bird
} else {
animal.swim(); // animal is narrowed to Fish
}
}
// Complex property checking
interface Rectangle {
width: number;
height: number;
}
interface Circle {
radius: number;
}
type Shape2D = Rectangle | Circle;
function getArea(shape: Shape2D): number {
if ('width' in shape && 'height' in shape) {
return shape.width * shape.height; // shape is Rectangle
}
if ('radius' in shape) {
return Math.PI * shape.radius ** 2; // shape is Circle
}
// This should never happen with proper types
throw new Error('Unknown shape');
}
// Defensive property checking
function safePropertyAccess(obj: unknown): string {
if (
typeof obj === 'object' &&
obj !== null &&
'toString' in obj &&
typeof obj.toString === 'function'
) {
return obj.toString();
}
return 'Cannot convert to string';
}
Generic Type Checking
Generic Type Guards
// Generic type guard functions
function isType<T>(
value: unknown,
validator: (v: unknown) => v is T
): value is T {
return validator(value);
}
// Generic array type guard
function isArrayOfType<T>(
value: unknown,
itemValidator: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemValidator);
}
// Generic object type guard
function isObjectWithShape<T extends Record<string, unknown>>(
value: unknown,
shape: { [K in keyof T]: (v: unknown) => v is T[K] }
): value is T {
if (typeof value !== 'object' || value === null) {
return false;
}
const obj = value as Record<string, unknown>;
for (const [key, validator] of Object.entries(shape)) {
if (!(key in obj) || !validator(obj[key])) {
return false;
}
}
return true;
}
// Usage examples
interface User {
id: number;
name: string;
email: string;
}
const userValidator = {
id: (v: unknown): v is number => typeof v === 'number',
name: (v: unknown): v is string => typeof v === 'string',
email: (v: unknown): v is string => typeof v === 'string' && v.includes('@')
};
function validateUser(data: unknown): data is User {
return isObjectWithShape(data, userValidator);
}
function validateUsers(data: unknown): data is User[] {
return isArrayOfType(data, validateUser);
}
Constrained Generics
// Constrained generic type checking
interface Identifiable {
id: string | number;
}
function isIdentifiable<T extends Identifiable>(
value: unknown
): value is T {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
(typeof (value as any).id === 'string' || typeof (value as any).id === 'number')
);
}
// Generic type checking with multiple constraints
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Named {
name: string;
}
function isTimestampedAndNamed<T extends Timestamped & Named>(
value: unknown
): value is T {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'createdAt' in value &&
'updatedAt' in value &&
typeof (value as any).name === 'string' &&
(value as any).createdAt instanceof Date &&
(value as any).updatedAt instanceof Date
);
}
// Conditional generic type checking
type ApiResult<T> = T extends string
? { message: T }
: T extends number
? { code: T }
: { data: T };
function isApiResult<T>(
value: unknown,
originalType: T
): value is ApiResult<T> {
if (typeof value !== 'object' || value === null) {
return false;
}
if (typeof originalType === 'string') {
return 'message' in value;
}
if (typeof originalType === 'number') {
return 'code' in value;
}
return 'data' in value;
}
Union and Intersection Type Checking
Union Types
// Union type checking strategies
type StringOrNumber = string | number;
type StringOrNumberOrBoolean = string | number | boolean;
function processUnion(value: StringOrNumber): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
// TypeScript knows value is number here
return value.toFixed(2);
}
// Complex union type checking
type Success<T> = { success: true; data: T };
type Failure = { success: false; error: string };
type Result<T> = Success<T> | Failure;
function isSuccess<T>(result: Result<T>): result is Success<T> {
return result.success === true;
}
function isFailure<T>(result: Result<T>): result is Failure {
return result.success === false;
}
function handleResult<T>(result: Result<T>): string {
if (isSuccess(result)) {
return `Success: ${JSON.stringify(result.data)}`;
}
return `Error: ${result.error}`;
}
// Union with null checking
type NullableString = string | null;
type OptionalString = string | undefined;
type NullishString = string | null | undefined;
function handleNullableString(value: NullableString): string {
if (value === null) {
return 'null';
}
return value; // value is narrowed to string
}
function handleOptionalString(value: OptionalString): string {
if (value === undefined) {
return 'undefined';
}
return value; // value is narrowed to string
}
function handleNullishString(value: NullishString): string {
if (value == null) { // checks both null and undefined
return 'nullish';
}
return value; // value is narrowed to string
}
Intersection Types
// Intersection type checking
interface Readable {
read(): string;
}
interface Writable {
write(data: string): void;
}
type ReadWrite = Readable & Writable;
function isReadable(obj: unknown): obj is Readable {
return (
typeof obj === 'object' &&
obj !== null &&
'read' in obj &&
typeof (obj as any).read === 'function'
);
}
function isWritable(obj: unknown): obj is Writable {
return (
typeof obj === 'object' &&
obj !== null &&
'write' in obj &&
typeof (obj as any).write === 'function'
);
}
function isReadWrite(obj: unknown): obj is ReadWrite {
return isReadable(obj) && isWritable(obj);
}
// Complex intersection types
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: string;
department: string;
}
interface Manager {
managerId: string;
teamSize: number;
}
type EmployeePerson = Person & Employee;
type ManagerPerson = Person & Employee & Manager;
function isEmployeePerson(obj: unknown): obj is EmployeePerson {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'age' in obj &&
'employeeId' in obj &&
'department' in obj &&
typeof (obj as any).name === 'string' &&
typeof (obj as any).age === 'number' &&
typeof (obj as any).employeeId === 'string' &&
typeof (obj as any).department === 'string'
);
}
function isManagerPerson(obj: unknown): obj is ManagerPerson {
return (
isEmployeePerson(obj) &&
'managerId' in obj &&
'teamSize' in obj &&
typeof (obj as any).managerId === 'string' &&
typeof (obj as any).teamSize === 'number'
);
}
Object and Array Type Checking
Object Type Checking
typescript
// Deep object type checking
interface NestedObject {
level1: {
level2: {
value: string;
items: number[];
};
};
}
function isNestedObject(obj: unknown): obj is NestedObject {
return (
typeof obj === 'object' &&
obj !== null &&
'level1' in obj &&
typeof (obj as any).level1 === 'object' &&
(obj as any).level1 !== null &&
'level2' in (obj as any).level1 &&
typeof (obj as any).level1.level2 === 'object' &&
(obj as any).level1.level2 !== null &&
'value' in (obj as any).level1.level2 &&
'items' in (obj as any).level1.level2 &&
typeof (obj as any).level1.level2.value === 'string' &&
Array.isArray((obj as any).level1.level2.items) &&
(obj as any).level1.level2.items.every((item: unknown) => typeof item === 'number')
);
}
// Record type checking
type StringRecord = Record<string, string>;
type NumberRecord = Record<string, number>;
type MixedRecord = Record<string, string | number>;
function isStringRecord(obj: unknown): obj is StringRecord {
return (
typeof obj === 'object' &&
obj !== null &&
!Array.isArray(obj) &&
Object.values(obj).every(value => typeof value === 'string')
);
}
function isNumberRecord(obj: unknown): obj is NumberRecord {
return (
typeof obj === 'object' &&
obj !== null &&
!Array.isArray(obj) &&
Object.values(obj).every(value => typeof value === 'number')
);
}
function isMixedRecord(obj: unknown): obj is MixedRecord {
return (
typeof obj === 'object' &&
obj !== null &&
!Array.isArray(obj) &&
Object.values(obj).every(value
Top comments (0)