Prerequisites
- Deep understanding of TypeScript union types and type narrowing 📝
- Experience with conditional types and type guards ⚡
- Knowledge of JavaScript runtime type checking patterns 💻
What you'll learn
- Create custom type predicates for complex type checking 🎯
- Build intelligent type guards that narrow types effectively 🏗️
- Implement runtime validation with compile-time type safety 🐛
- Apply type predicates to real-world data validation scenarios ✨
🎯 Introduction
Welcome to the type detective agency of TypeScript! 🔍 This tutorial explores the powerful world of type predicates and user-defined type guards, where you’ll learn to create intelligent runtime type checking systems that work seamlessly with TypeScript’s compile-time type system.
You’ll discover how to build custom type guards that can intelligently narrow union types, validate complex data structures, and provide runtime safety while maintaining excellent TypeScript integration. Whether you’re building API validation layers 🌐, processing user input 📝, or working with external data sources 📊, type predicates provide the bridge between runtime reality and compile-time safety.
By the end of this tutorial, you’ll be crafting sophisticated type detection systems that make your code both runtime-safe and compile-time intelligent! Let’s guard those types! 🛡️
📚 Understanding Type Predicates
🤔 What Are Type Predicates?
Type predicates are special function return types that tell TypeScript how to narrow types based on runtime checks. They use the is
keyword to create a contract between runtime behavior and compile-time types:
// 🌟 Basic type predicate syntax
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// 🎯 Using the type predicate
function processValue(input: unknown) {
if (isString(input)) {
// ✨ TypeScript now knows input is string
console.log(input.toUpperCase()); // No type error!
console.log(input.length); // Access string properties
}
}
// 🔍 Without type predicate (for comparison)
function isStringBad(value: unknown): boolean {
return typeof value === 'string';
}
function processValueBad(input: unknown) {
if (isStringBad(input)) {
// ❌ TypeScript still thinks input is unknown
// console.log(input.toUpperCase()); // Type error!
}
}
🏗️ Type Predicate Fundamentals
// 🎯 Basic type predicates for primitives
function isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
function isFunction(value: unknown): value is Function {
return typeof value === 'function';
}
// 🌟 Array type predicates
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function isNumberArray(value: unknown): value is number[] {
return Array.isArray(value) && value.every(item => typeof item === 'number');
}
// ✅ Using type predicates
function demonstratePredicates(data: unknown) {
if (isNumber(data)) {
console.log(data.toFixed(2)); // ✨ Number methods available
}
if (isStringArray(data)) {
console.log(data.map(s => s.toUpperCase())); // ✨ String array methods
}
if (isFunction(data)) {
const result = data(); // ✨ Can call as function
}
}
// 🔄 Null and undefined predicates
function isNull(value: unknown): value is null {
return value === null;
}
function isUndefined(value: unknown): value is undefined {
return value === undefined;
}
function isNullish(value: unknown): value is null | undefined {
return value === null || value === undefined;
}
function isNotNullish<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// 🎨 Using nullish predicates
function processData(input: string | null | undefined) {
if (isNotNullish(input)) {
// ✨ TypeScript knows input is string
console.log(input.substring(0, 10));
}
}
🧮 Complex Object Type Predicates
// 🎯 Object type predicates
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).id === 'number' &&
typeof (value as any).name === 'string' &&
typeof (value as any).email === 'string'
);
}
function isProduct(value: unknown): value is Product {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).id === 'number' &&
typeof (value as any).title === 'string' &&
typeof (value as any).price === 'number'
);
}
// 🌟 Generic object validation
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return typeof obj === 'object' && obj !== null && key in obj;
}
function hasStringProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, string> {
return hasProperty(obj, key) && typeof (obj as any)[key] === 'string';
}
function hasNumberProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, number> {
return hasProperty(obj, key) && typeof (obj as any)[key] === 'number';
}
// ✅ Using object predicates
function processEntity(data: unknown) {
if (isUser(data)) {
// ✨ TypeScript knows data is User
console.log(`User: ${data.name} (${data.email})`);
} else if (isProduct(data)) {
// ✨ TypeScript knows data is Product
console.log(`Product: ${data.title} - $${data.price}`);
}
}
function validateObject(obj: unknown) {
if (hasStringProperty(obj, 'name')) {
// ✨ TypeScript knows obj has name: string
console.log(`Name: ${obj.name}`);
}
if (hasNumberProperty(obj, 'age')) {
// ✨ TypeScript knows obj has age: number
console.log(`Age: ${obj.age} years old`);
}
}
🔄 Advanced Type Guard Patterns
🎯 Discriminated Union Type Guards
// 🌟 Discriminated union types
interface LoadingState {
status: 'loading';
progress: number;
}
interface SuccessState {
status: 'success';
data: string;
}
interface ErrorState {
status: 'error';
message: string;
code: number;
}
type AppState = LoadingState | SuccessState | ErrorState;
// 🔍 Discriminated union type guards
function isLoadingState(state: AppState): state is LoadingState {
return state.status === 'loading';
}
function isSuccessState(state: AppState): state is SuccessState {
return state.status === 'success';
}
function isErrorState(state: AppState): state is ErrorState {
return state.status === 'error';
}
// ✨ Using discriminated union guards
function handleAppState(state: AppState) {
if (isLoadingState(state)) {
console.log(`Loading: ${state.progress}%`);
} else if (isSuccessState(state)) {
console.log(`Success: ${state.data}`);
} else if (isErrorState(state)) {
console.log(`Error ${state.code}: ${state.message}`);
}
}
// 🎨 Generic discriminated union guard
function hasStatus<T extends string>(
state: { status: string },
status: T
): state is { status: T } & typeof state {
return state.status === status;
}
function handleStateGeneric(state: AppState) {
if (hasStatus(state, 'loading')) {
// ✨ TypeScript knows it's LoadingState
console.log(`Progress: ${state.progress}`);
}
}
// 🔄 Complex discriminated unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
function isCircle(shape: Shape): shape is Extract<Shape, { kind: 'circle' }> {
return shape.kind === 'circle';
}
function isRectangle(shape: Shape): shape is Extract<Shape, { kind: 'rectangle' }> {
return shape.kind === 'rectangle';
}
function calculateArea(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius ** 2;
} else if (isRectangle(shape)) {
return shape.width * shape.height;
} else {
// TypeScript knows it's triangle
return 0.5 * shape.base * shape.height;
}
}
🧩 Class Instance Type Guards
// 🎯 Class-based type guards
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number,
public url: string
) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
// 🔍 Instance type predicates
function isNetworkError(error: unknown): error is NetworkError {
return error instanceof NetworkError;
}
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
function isError(value: unknown): value is Error {
return value instanceof Error;
}
// ✨ Using class type guards
function handleError(error: unknown) {
if (isNetworkError(error)) {
console.log(`Network error ${error.statusCode} at ${error.url}: ${error.message}`);
} else if (isValidationError(error)) {
console.log(`Validation error in ${error.field}: ${error.message}`);
} else if (isError(error)) {
console.log(`Generic error: ${error.message}`);
} else {
console.log('Unknown error:', error);
}
}
// 🌟 Generic instance checker
function isInstanceOf<T extends new (...args: any[]) => any>(
value: unknown,
constructor: T
): value is InstanceType<T> {
return value instanceof constructor;
}
// Usage with generic instance checker
function processValue(value: unknown) {
if (isInstanceOf(value, Date)) {
console.log(value.toISOString()); // ✨ Date methods available
}
if (isInstanceOf(value, RegExp)) {
console.log(value.source); // ✨ RegExp properties available
}
}
// 🎨 Built-in type guards
function isDate(value: unknown): value is Date {
return value instanceof Date && !isNaN(value.getTime());
}
function isRegExp(value: unknown): value is RegExp {
return value instanceof RegExp;
}
function isMap(value: unknown): value is Map<unknown, unknown> {
return value instanceof Map;
}
function isSet(value: unknown): value is Set<unknown> {
return value instanceof Set;
}
function isPromise<T = unknown>(value: unknown): value is Promise<T> {
return value instanceof Promise ||
(typeof value === 'object' &&
value !== null &&
typeof (value as any).then === 'function');
}
🔄 Nested and Composite Type Guards
// 🎯 Nested object validation
interface Address {
street: string;
city: string;
zipCode: string;
}
interface Person {
name: string;
age: number;
address: Address;
hobbies: string[];
}
function isAddress(value: unknown): value is Address {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).street === 'string' &&
typeof (value as any).city === 'string' &&
typeof (value as any).zipCode === 'string'
);
}
function isPerson(value: unknown): value is Person {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).name === 'string' &&
typeof (value as any).age === 'number' &&
isAddress((value as any).address) &&
isStringArray((value as any).hobbies)
);
}
// 🌟 Composite type validation
type APIResponse<T> = {
success: true;
data: T;
} | {
success: false;
error: string;
code: number;
};
function isSuccessResponse<T>(
response: APIResponse<T>,
dataValidator: (data: unknown) => data is T
): response is Extract<APIResponse<T>, { success: true }> {
return response.success === true && dataValidator(response.data);
}
function isErrorResponse<T>(
response: APIResponse<T>
): response is Extract<APIResponse<T>, { success: false }> {
return response.success === false;
}
// ✅ Using composite guards
async function processAPIResponse(response: APIResponse<Person>) {
if (isSuccessResponse(response, isPerson)) {
// ✨ TypeScript knows response.data is Person
console.log(`Welcome ${response.data.name}!`);
console.log(`Lives in ${response.data.address.city}`);
} else if (isErrorResponse(response)) {
// ✨ TypeScript knows response has error info
console.log(`Error ${response.code}: ${response.error}`);
}
}
// 🔄 Array validation with type guards
function isArrayOf<T>(
value: unknown,
itemValidator: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemValidator);
}
function isPersonArray(value: unknown): value is Person[] {
return isArrayOf(value, isPerson);
}
// 🎨 Optional property validation
function hasOptionalStringProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, string | undefined> {
return (
typeof obj === 'object' &&
obj !== null &&
(!(key in obj) || typeof (obj as any)[key] === 'string')
);
}
interface OptionalUser {
id: number;
name: string;
nickname?: string;
bio?: string;
}
function isOptionalUser(value: unknown): value is OptionalUser {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).id === 'number' &&
typeof (value as any).name === 'string' &&
hasOptionalStringProperty(value, 'nickname') &&
hasOptionalStringProperty(value, 'bio')
);
}
🛡️ Runtime Validation and Type Safety
🔍 JSON and External Data Validation
// 🌟 JSON validation with type predicates
function parseJSON<T>(
json: string,
validator: (data: unknown) => data is T
): T | null {
try {
const parsed = JSON.parse(json);
return validator(parsed) ? parsed : null;
} catch {
return null;
}
}
// 🎯 API response validation
interface UserProfile {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
avatar?: string;
};
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
function isUserProfile(value: unknown): value is UserProfile {
if (typeof value !== 'object' || value === null) return false;
const obj = value as any;
return (
typeof obj.id === 'number' &&
typeof obj.username === 'string' &&
typeof obj.email === 'string' &&
typeof obj.profile === 'object' &&
obj.profile !== null &&
typeof obj.profile.firstName === 'string' &&
typeof obj.profile.lastName === 'string' &&
(obj.profile.avatar === undefined || typeof obj.profile.avatar === 'string') &&
typeof obj.preferences === 'object' &&
obj.preferences !== null &&
(obj.preferences.theme === 'light' || obj.preferences.theme === 'dark') &&
typeof obj.preferences.notifications === 'boolean'
);
}
// ✨ Safe API data processing
async function fetchUserProfile(userId: number): Promise<UserProfile | null> {
try {
const response = await fetch(`/api/users/${userId}`);
const json = await response.text();
return parseJSON(json, isUserProfile);
} catch {
return null;
}
}
// 🔄 Form data validation
interface FormData {
email: string;
password: string;
confirmPassword: string;
terms: boolean;
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function isValidPassword(password: string): boolean {
return password.length>= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password);
}
function isFormData(value: unknown): value is FormData {
if (typeof value !== 'object' || value === null) return false;
const obj = value as any;
return (
typeof obj.email === 'string' &&
isValidEmail(obj.email) &&
typeof obj.password === 'string' &&
isValidPassword(obj.password) &&
typeof obj.confirmPassword === 'string' &&
obj.password === obj.confirmPassword &&
typeof obj.terms === 'boolean' &&
obj.terms === true
);
}
// 🎨 Configuration validation
interface AppConfig {
apiUrl: string;
timeout: number;
features: {
darkMode: boolean;
analytics: boolean;
experiments: string[];
};
logging: {
level: 'debug' | 'info' | 'warn' | 'error';
destination: 'console' | 'file' | 'remote';
};
}
function isAppConfig(value: unknown): value is AppConfig {
if (typeof value !== 'object' || value === null) return false;
const obj = value as any;
const validLogLevels = ['debug', 'info', 'warn', 'error'];
const validDestinations = ['console', 'file', 'remote'];
return (
typeof obj.apiUrl === 'string' &&
typeof obj.timeout === 'number' &&
obj.timeout> 0 &&
typeof obj.features === 'object' &&
obj.features !== null &&
typeof obj.features.darkMode === 'boolean' &&
typeof obj.features.analytics === 'boolean' &&
isStringArray(obj.features.experiments) &&
typeof obj.logging === 'object' &&
obj.logging !== null &&
validLogLevels.includes(obj.logging.level) &&
validDestinations.includes(obj.logging.destination)
);
}
function loadConfig(configJson: string): AppConfig | null {
return parseJSON(configJson, isAppConfig);
}
🎯 Type Guard Utilities and Helpers
// 🌟 Type guard utility library
class TypeGuards {
// Primitive type guards
static isString = (value: unknown): value is string =>
typeof value === 'string';
static isNumber = (value: unknown): value is number =>
typeof value === 'number' && !isNaN(value);
static isBoolean = (value: unknown): value is boolean =>
typeof value === 'boolean';
static isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value);
// Array type guards
static isArrayOf<T>(
validator: (item: unknown) => item is T
) {
return (value: unknown): value is T[] =>
Array.isArray(value) && value.every(validator);
}
// Object property validation
static hasProperties<T extends Record<string, (value: unknown) => boolean>>(
validators: T
) {
return (value: unknown): value is {
[K in keyof T]: T[K] extends (value: unknown) => value is infer R ? R : never
} => {
if (!TypeGuards.isObject(value)) return false;
return Object.entries(validators).every(([key, validator]) =>
key in value && validator((value as any)[key])
);
};
}
// Union type validation
static oneOf<T extends readonly any[]>(
...validators: {
[K in keyof T]: (value: unknown) => value is T[K]
}
) {
return (value: unknown): value is T[number] =>
validators.some(validator => validator(value));
}
// Optional property validation
static optional<T>(validator: (value: unknown) => value is T) {
return (value: unknown): value is T | undefined =>
value === undefined || validator(value);
}
// Nullable validation
static nullable<T>(validator: (value: unknown) => value is T) {
return (value: unknown): value is T | null =>
value === null || validator(value);
}
}
// ✅ Using the TypeGuards utility
const isUser = TypeGuards.hasProperties({
id: TypeGuards.isNumber,
name: TypeGuards.isString,
email: TypeGuards.isString,
age: TypeGuards.optional(TypeGuards.isNumber),
});
const isStringOrNumber = TypeGuards.oneOf(
TypeGuards.isString,
TypeGuards.isNumber
);
const isUserArray = TypeGuards.isArrayOf(isUser);
// 🔄 Schema validation builder
type SchemaValidator<T> = (value: unknown) => value is T;
function createSchema<T>(): {
string(): SchemaValidator<string>;
number(): SchemaValidator<number>;
boolean(): SchemaValidator<boolean>;
array<U>(itemValidator: SchemaValidator<U>): SchemaValidator<U[]>;
object<O extends Record<string, SchemaValidator<any>>>(
schema: O
): SchemaValidator<{
[K in keyof O]: O[K] extends SchemaValidator<infer U> ? U : never
}>;
optional<U>(validator: SchemaValidator<U>): SchemaValidator<U | undefined>;
union<U extends readonly any[]>(...validators: {
[K in keyof U]: SchemaValidator<U[K]>
}): SchemaValidator<U[number]>;
} {
return {
string: () => TypeGuards.isString,
number: () => TypeGuards.isNumber,
boolean: () => TypeGuards.isBoolean,
array: <U>(itemValidator: SchemaValidator<U>) =>
TypeGuards.isArrayOf(itemValidator),
object: <O extends Record<string, SchemaValidator<any>>>(schema: O) =>
TypeGuards.hasProperties(schema),
optional: <U>(validator: SchemaValidator<U>) =>
TypeGuards.optional(validator),
union: <U extends readonly any[]>(...validators: {
[K in keyof U]: SchemaValidator<U[K]>
}) => TypeGuards.oneOf(...validators),
};
}
// 🎨 Schema builder usage
const schema = createSchema();
const isPost = schema.object({
id: schema.number(),
title: schema.string(),
content: schema.string(),
published: schema.boolean(),
tags: schema.array(schema.string()),
author: schema.object({
id: schema.number(),
name: schema.string(),
}),
publishedAt: schema.optional(schema.string()),
});
function processPost(data: unknown) {
if (isPost(data)) {
// ✨ Full type safety with inferred types
console.log(`Post: ${data.title} by ${data.author.name}`);
console.log(`Tags: ${data.tags.join(', ')}`);
if (data.publishedAt) {
console.log(`Published: ${data.publishedAt}`);
}
}
}
🎮 Real-World Applications and Patterns
🌐 API Response Validation
// 🎯 Generic API response wrapper
interface APISuccess<T> {
success: true;
data: T;
timestamp: string;
}
interface APIError {
success: false;
error: {
code: string;
message: string;
details?: Record<string, unknown>;
};
timestamp: string;
}
type APIResponse<T> = APISuccess<T> | APIError;
function isAPISuccess<T>(
response: APIResponse<T>,
dataValidator: (data: unknown) => data is T
): response is APISuccess<T> {
return response.success && dataValidator(response.data);
}
function isAPIError<T>(response: APIResponse<T>): response is APIError {
return !response.success;
}
// 🌟 Specific API validators
interface TodoItem {
id: number;
title: string;
completed: boolean;
userId: number;
createdAt: string;
updatedAt: string;
}
function isTodoItem(value: unknown): value is TodoItem {
return TypeGuards.hasProperties({
id: TypeGuards.isNumber,
title: TypeGuards.isString,
completed: TypeGuards.isBoolean,
userId: TypeGuards.isNumber,
createdAt: TypeGuards.isString,
updatedAt: TypeGuards.isString,
})(value);
}
const isTodoArray = TypeGuards.isArrayOf(isTodoItem);
// ✨ Safe API client
class SafeAPIClient {
async fetchTodos(userId: number): Promise<TodoItem[] | string> {
try {
const response = await fetch(`/api/users/${userId}/todos`);
const json = await response.json();
if (isAPISuccess(json, isTodoArray)) {
return json.data;
} else if (isAPIError(json)) {
return `API Error ${json.error.code}: ${json.error.message}`;
} else {
return 'Invalid API response format';
}
} catch (error) {
return `Network error: ${error}`;
}
}
async createTodo(todo: Omit<TodoItem, 'id' | 'createdAt' | 'updatedAt'>): Promise<TodoItem | string> {
try {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo),
});
const json = await response.json();
if (isAPISuccess(json, isTodoItem)) {
return json.data;
} else if (isAPIError(json)) {
return `Failed to create todo: ${json.error.message}`;
} else {
return 'Invalid response format';
}
} catch (error) {
return `Request failed: ${error}`;
}
}
}
🎨 Event System with Type Guards
// 🔄 Type-safe event system
interface UserLoginEvent {
type: 'user:login';
payload: {
userId: number;
username: string;
timestamp: string;
};
}
interface UserLogoutEvent {
type: 'user:logout';
payload: {
userId: number;
sessionDuration: number;
};
}
interface DataUpdateEvent {
type: 'data:update';
payload: {
entityType: string;
entityId: number;
changes: Record<string, unknown>;
};
}
type AppEvent = UserLoginEvent | UserLogoutEvent | DataUpdateEvent;
// 🎯 Event type guards
function isUserLoginEvent(event: AppEvent): event is UserLoginEvent {
return event.type === 'user:login';
}
function isUserLogoutEvent(event: AppEvent): event is UserLogoutEvent {
return event.type === 'user:logout';
}
function isDataUpdateEvent(event: AppEvent): event is DataUpdateEvent {
return event.type === 'data:update';
}
function isEventOfType<T extends AppEvent['type']>(
type: T
) {
return (event: AppEvent): event is Extract<AppEvent, { type: T }> =>
event.type === type;
}
// ✨ Type-safe event handler
class EventManager {
private handlers = new Map<string, Array<(event: any) => void>>();
on<T extends AppEvent>(
predicate: (event: AppEvent) => event is T,
handler: (event: T) => void
) {
const key = predicate.toString();
if (!this.handlers.has(key)) {
this.handlers.set(key, []);
}
this.handlers.get(key)!.push(handler);
}
emit(event: AppEvent) {
for (const [predicateStr, handlers] of this.handlers) {
// In real implementation, we'd need to store the actual predicate
// This is simplified for demonstration
handlers.forEach(handler => handler(event));
}
}
}
// Usage
const eventManager = new EventManager();
eventManager.on(isUserLoginEvent, (event) => {
// ✨ TypeScript knows event is UserLoginEvent
console.log(`User ${event.payload.username} logged in`);
});
eventManager.on(isDataUpdateEvent, (event) => {
// ✨ TypeScript knows event is DataUpdateEvent
console.log(`${event.payload.entityType} ${event.payload.entityId} updated`);
});
🛠️ Plugin System with Type Guards
// 🌟 Plugin architecture with type safety
interface BasePlugin {
name: string;
version: string;
enabled: boolean;
}
interface AuthPlugin extends BasePlugin {
type: 'auth';
config: {
provider: 'oauth' | 'jwt' | 'session';
secret: string;
expiry: number;
};
authenticate(token: string): Promise<boolean>;
}
interface CachePlugin extends BasePlugin {
type: 'cache';
config: {
engine: 'redis' | 'memory' | 'file';
ttl: number;
maxSize: number;
};
get(key: string): Promise<unknown>;
set(key: string, value: unknown): Promise<void>;
}
interface LoggingPlugin extends BasePlugin {
type: 'logging';
config: {
level: 'debug' | 'info' | 'warn' | 'error';
output: 'console' | 'file';
format: 'json' | 'text';
};
log(level: string, message: string): void;
}
type Plugin = AuthPlugin | CachePlugin | LoggingPlugin;
// 🔍 Plugin type guards
function isAuthPlugin(plugin: Plugin): plugin is AuthPlugin {
return plugin.type === 'auth';
}
function isCachePlugin(plugin: Plugin): plugin is CachePlugin {
return plugin.type === 'cache';
}
function isLoggingPlugin(plugin: Plugin): plugin is LoggingPlugin {
return plugin.type === 'logging';
}
function isPluginOfType<T extends Plugin['type']>(
type: T
) {
return (plugin: Plugin): plugin is Extract<Plugin, { type: T }> =>
plugin.type === type;
}
// ✨ Plugin manager with type safety
class PluginManager {
private plugins = new Map<string, Plugin>();
register(plugin: Plugin) {
this.plugins.set(plugin.name, plugin);
}
getPlugin<T extends Plugin['type']>(
name: string,
type: T
): Extract<Plugin, { type: T }> | null {
const plugin = this.plugins.get(name);
if (!plugin) return null;
const typePredicate = isPluginOfType(type);
return typePredicate(plugin) ? plugin : null;
}
getAuthPlugin(name: string): AuthPlugin | null {
const plugin = this.plugins.get(name);
return plugin && isAuthPlugin(plugin) ? plugin : null;
}
getCachePlugin(name: string): CachePlugin | null {
const plugin = this.plugins.get(name);
return plugin && isCachePlugin(plugin) ? plugin : null;
}
getEnabledPlugins(): Plugin[] {
return Array.from(this.plugins.values()).filter(p => p.enabled);
}
getPluginsByType<T extends Plugin['type']>(
type: T
): Array<Extract<Plugin, { type: T }>> {
const typePredicate = isPluginOfType(type);
return Array.from(this.plugins.values()).filter(typePredicate);
}
}
// Usage
const pluginManager = new PluginManager();
const authPlugin: AuthPlugin = {
name: 'jwt-auth',
version: '1.0.0',
enabled: true,
type: 'auth',
config: {
provider: 'jwt',
secret: 'secret-key',
expiry: 3600,
},
async authenticate(token: string) {
// JWT validation logic
return true;
},
};
pluginManager.register(authPlugin);
const auth = pluginManager.getAuthPlugin('jwt-auth');
if (auth) {
// ✨ TypeScript knows auth is AuthPlugin
console.log(`Auth provider: ${auth.config.provider}`);
await auth.authenticate('token123');
}
🎓 Key Takeaways
You’ve mastered the art of type predicates and user-defined type guards! Here’s what you now command:
- ✅ Custom type predicates for intelligent runtime type checking 💪
- ✅ Discriminated union guards for complex state management 🛡️
- ✅ Object validation patterns for API and form data safety 🎯
- ✅ Advanced type guard utilities and helper libraries 🐛
- ✅ Runtime validation with compile-time type guarantees 🚀
- ✅ Real-world applications in APIs, events, and plugin systems ✨
- ✅ Type-safe architectures that bridge runtime and compile-time 🔄
Remember: Type predicates are the bridge between JavaScript’s dynamic nature and TypeScript’s static safety! 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve become a master of type predicates and type guards!
Here’s what to do next:
- 💻 Build robust API validation layers with custom type predicates
- 🏗️ Create type-safe event systems and plugin architectures
- 📚 Move on to our next tutorial: Assertion Functions - Custom Type Assertions
- 🌟 Implement schema validation libraries with type inference
- 🔍 Explore advanced runtime type checking and validation patterns
- 🎯 Build intelligent data processing pipelines with type safety
- 🚀 Push the boundaries of runtime-compile time type integration
Remember: You now possess the power to create intelligent type checking systems that work at both runtime and compile time! Use this knowledge to build incredibly safe and robust applications. 🚀
Happy type guarding! 🎉🔍✨