
Mastering TypeScript: Advanced Patterns and Best Practices
Dive deep into advanced TypeScript patterns, utility types, and best practices for building type-safe applications that scale with your team and codebase.
Mastering TypeScript: Advanced Patterns and Best Practices
TypeScript has evolved from a simple type layer over JavaScript to a sophisticated language that enables developers to build robust, maintainable, and scalable applications. This comprehensive guide explores advanced patterns, architectural strategies, and best practices that will elevate your TypeScript expertise to new heights.
The TypeScript Advantage: Beyond Basic Types
TypeScript's true power lies not in its basic type annotations, but in its advanced type system that enables compile-time guarantees, improved developer experience, and architectural patterns that scale with your application's complexity.
Why Advanced TypeScript Matters
- Compile-time Safety: Catch errors before they reach production
- Enhanced IDE Support: Superior autocomplete, refactoring, and navigation
- Self-Documenting Code: Types serve as living documentation
- Refactoring Confidence: Large-scale changes with confidence
- Team Collaboration: Shared understanding through explicit contracts
Advanced Type System Fundamentals
Conditional Types: The Foundation of Type-Level Programming
Conditional types allow you to create types that depend on conditions, enabling powerful type transformations:
// Basic conditional type
type IsString<T> = T extends string ? true : false
type Test1 = IsString<string> // true
type Test2 = IsString<number> // false
// Advanced conditional type with inference
type ExtractArrayType<T> = T extends (infer U)[] ? U : never
type StringArray = ExtractArrayType<string[]> // string
type NumberArray = ExtractArrayType<number[]> // number
// Nested conditional types for complex logic
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P]
}
interface User {
id: number
name: string
profile: {
email: string
preferences: {
theme: string
notifications: boolean
}
}
updateProfile: (data: any) => void
}
type ReadonlyUser = DeepReadonly<User>
// All properties are readonly except functions
Template Literal Types: String Manipulation at the Type Level
Template literal types enable sophisticated string manipulation and validation:
// Basic template literal types
type Greeting<T extends string> = `Hello, ${T}!`
type WelcomeMessage = Greeting<"World"> // "Hello, World!"
// Advanced string manipulation
type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
? `${P1}${Uppercase<P2>}${CamelCase<P3>}`
: S
type SnakeToCamel = CamelCase<"user_profile_settings"> // "userProfileSettings"
// Route parameter extraction
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractRouteParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {}
type UserRoute = ExtractRouteParams<"/users/:id/posts/:postId">
// { id: string; postId: string }
// SQL-like query builder types
type SelectClause<T, K extends keyof T> = {
[P in K]: T[P]
}
type WhereClause<T> = {
[K in keyof T]?: T[K] | { $gt?: T[K]; $lt?: T[K]; $in?: T[K][] }
}
interface QueryBuilder<T> {
select<K extends keyof T>(...fields: K[]): QueryBuilder<SelectClause<T, K>>
where(conditions: WhereClause<T>): QueryBuilder<T>
limit(count: number): QueryBuilder<T>
execute(): Promise<T[]>
}
Mapped Types: Transforming Type Structures
Mapped types allow you to create new types by transforming properties of existing types:
// Advanced mapped type patterns
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
}[keyof T]
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T]
// Separate optional and required properties
type SplitOptionalRequired<T> = {
optional: Pick<T, OptionalKeys<T>>
required: Pick<T, RequiredKeys<T>>
}
interface UserForm {
name: string
email: string
age?: number
bio?: string
}
type UserFormSplit = SplitOptionalRequired<UserForm>
// {
// optional: { age?: number; bio?: string }
// required: { name: string; email: string }
// }
// Recursive type transformation
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Function
? T[P]
: DeepPartial<T[P]>
: T[P]
}
// Type-safe object path access
type PathsToStringProps<T> = T extends string
? []
: {
[K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>]
}[Extract<keyof T, string>]
type Join<T extends string[], D extends string> = T extends readonly [
infer F,
...infer R
]
? F extends string
? R extends readonly string[]
? R['length'] extends 0
? F
: `${F}${D}${Join<R, D>}`
: never
: never
: never
type DotNotation<T> = Join<PathsToStringProps<T>, '.'>
interface NestedObject {
user: {
profile: {
name: string
settings: {
theme: string
}
}
}
}
type Paths = DotNotation<NestedObject>
// "user" | "user.profile" | "user.profile.name" | "user.profile.settings" | "user.profile.settings.theme"
Utility Types Mastery
Built-in Utility Types and Their Advanced Usage
// Advanced utility type combinations
interface User {
id: number
name: string
email: string
password: string
createdAt: Date
updatedAt: Date
profile: {
bio: string
avatar: string
preferences: {
theme: 'light' | 'dark'
notifications: boolean
}
}
}
// Create a public user type
type PublicUser = Omit<User, 'password'> & {
profile: Omit<User['profile'], 'preferences'>
}
// Create update types with nested partial
type UserUpdate = {
[K in keyof User]?: K extends 'profile'
? {
[P in keyof User[K]]?: User[K][P] extends object
? Partial<User[K][P]>
: User[K][P]
}
: User[K]
}
// Function type utilities
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never
async function fetchUser(): Promise<User> {
return {} as User
}
type FetchedUser = AsyncReturnType<typeof fetchUser> // User
// Advanced parameter manipulation
type DropFirstParameter<T extends (...args: any) => any> =
T extends (first: any, ...rest: infer R) => any
? (...args: R) => ReturnType<T>
: never
function originalFunction(context: any, data: string, options: object) {
return { success: true }
}
type BoundFunction = DropFirstParameter<typeof originalFunction>
// (data: string, options: object) => { success: true }
Custom Utility Types for Domain-Specific Problems
// Event system utility types
type EventMap = {
'user:login': { userId: string; timestamp: Date }
'user:logout': { userId: string }
'order:created': { orderId: string; amount: number; items: string[] }
'order:cancelled': { orderId: string; reason: string }
}
type EventHandler<T extends keyof EventMap> = (data: EventMap[T]) => void | Promise<void>
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Set<EventHandler<K>>
} = {}
on<K extends keyof T>(event: K, handler: EventHandler<K>): void {
if (!this.listeners[event]) {
this.listeners[event] = new Set()
}
this.listeners[event]!.add(handler)
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const handlers = this.listeners[event]
if (handlers) {
handlers.forEach(handler => handler(data))
}
}
off<K extends keyof T>(event: K, handler: EventHandler<K>): void {
const handlers = this.listeners[event]
if (handlers) {
handlers.delete(handler)
}
}
}
// Usage with full type safety
const eventEmitter = new TypedEventEmitter<EventMap>()
eventEmitter.on('user:login', (data) => {
// data is typed as { userId: string; timestamp: Date }
console.log(`User ${data.userId} logged in at ${data.timestamp}`)
})
// Database query builder with type safety
type WhereOperators<T> = {
$eq?: T
$ne?: T
$gt?: T extends number | Date ? T : never
$gte?: T extends number | Date ? T : never
$lt?: T extends number | Date ? T : never
$lte?: T extends number | Date ? T : never
$in?: T[]
$nin?: T[]
$regex?: T extends string ? RegExp : never
}
type WhereCondition<T> = {
[K in keyof T]?: T[K] | WhereOperators<T[K]>
}
interface QueryBuilder<T> {
where(condition: WhereCondition<T>): QueryBuilder<T>
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>>
orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>
limit(count: number): QueryBuilder<T>
skip(count: number): QueryBuilder<T>
execute(): Promise<T[]>
}
// Form validation utility types
type ValidationRule<T> = {
required?: boolean
min?: T extends string ? number : T extends number ? T : never
max?: T extends string ? number : T extends number ? T : never
pattern?: T extends string ? RegExp : never
custom?: (value: T) => boolean | string
}
type FormValidation<T> = {
[K in keyof T]?: ValidationRule<T[K]>
}
type ValidationErrors<T> = {
[K in keyof T]?: string[]
}
class FormValidator<T extends Record<string, any>> {
constructor(private rules: FormValidation<T>) {}
validate(data: T): ValidationErrors<T> {
const errors: ValidationErrors<T> = {}
for (const field in this.rules) {
const rule = this.rules[field]
const value = data[field]
const fieldErrors: string[] = []
if (rule?.required && (value === undefined || value === null || value === '')) {
fieldErrors.push(`${field} is required`)
}
if (rule?.min !== undefined && typeof value === 'string' && value.length < rule.min) {
fieldErrors.push(`${field} must be at least ${rule.min} characters`)
}
if (rule?.pattern && typeof value === 'string' && !rule.pattern.test(value)) {
fieldErrors.push(`${field} format is invalid`)
}
if (rule?.custom) {
const customResult = rule.custom(value)
if (typeof customResult === 'string') {
fieldErrors.push(customResult)
} else if (!customResult) {
fieldErrors.push(`${field} is invalid`)
}
}
if (fieldErrors.length > 0) {
errors[field] = fieldErrors
}
}
return errors
}
}
Advanced Design Patterns
Builder Pattern with Type Safety
// Type-safe builder pattern with progressive disclosure
interface DatabaseConfig {
host: string
port: number
database: string
username: string
password: string
ssl: boolean
poolSize: number
timeout: number
}
type RequiredFields = 'host' | 'port' | 'database' | 'username' | 'password'
type OptionalFields = Exclude<keyof DatabaseConfig, RequiredFields>
class DatabaseConfigBuilder {
private config: Partial<DatabaseConfig> = {}
host(host: string): DatabaseConfigBuilder {
this.config.host = host
return this
}
port(port: number): DatabaseConfigBuilder {
this.config.port = port
return this
}
database(database: string): DatabaseConfigBuilder {
this.config.database = database
return this
}
credentials(username: string, password: string): DatabaseConfigBuilder {
this.config.username = username
this.config.password = password
return this
}
ssl(enabled: boolean = true): DatabaseConfigBuilder {
this.config.ssl = enabled
return this
}
poolSize(size: number): DatabaseConfigBuilder {
this.config.poolSize = size
return this
}
timeout(ms: number): DatabaseConfigBuilder {
this.config.timeout = ms
return this
}
build(): DatabaseConfig {
const required: RequiredFields[] = ['host', 'port', 'database', 'username', 'password']
for (const key of required) {
if (this.config[key] === undefined) {
throw new Error(`Missing required config: ${key}`)
}
}
return {
...this.config,
ssl: this.config.ssl ?? false,
poolSize: this.config.poolSize ?? 10,
timeout: this.config.timeout ?? 30000,
} as DatabaseConfig
}
}
// Usage with full type safety
const config = new DatabaseConfigBuilder()
.host('localhost')
.port(5432)
.database('myapp')
.credentials('user', 'password')
.ssl(true)
.poolSize(20)
.build()
State Machine Pattern with TypeScript
// Type-safe state machine implementation
type StateTransitions<States extends string, Events extends string> = {
[S in States]: {
[E in Events]?: States
}
}
type StateActions<States extends string, Context> = {
[S in States]?: {
onEnter?: (context: Context) => void | Promise<void>
onExit?: (context: Context) => void | Promise<void>
}
}
class StateMachine<
States extends string,
Events extends string,
Context extends Record<string, any>
> {
private currentState: States
private context: Context
constructor(
initialState: States,
initialContext: Context,
private transitions: StateTransitions<States, Events>,
private actions: StateActions<States, Context> = {}
) {
this.currentState = initialState
this.context = { ...initialContext }
}
async transition(event: Events): Promise<boolean> {
const nextState = this.transitions[this.currentState]?.[event]
if (!nextState) {
return false
}
// Execute exit action for current state
await this.actions[this.currentState]?.onExit?.(this.context)
// Update state
const previousState = this.currentState
this.currentState = nextState
// Execute enter action for new state
await this.actions[this.currentState]?.onEnter?.(this.context)
return true
}
getState(): States {
return this.currentState
}
getContext(): Readonly<Context> {
return { ...this.context }
}
updateContext(updates: Partial<Context>): void {
this.context = { ...this.context, ...updates }
}
}
// Order processing state machine example
type OrderStates = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
type OrderEvents = 'process' | 'ship' | 'deliver' | 'cancel'
interface OrderContext {
orderId: string
customerId: string
items: Array<{ id: string; quantity: number }>
total: number
shippingAddress?: string
trackingNumber?: string
}
const orderTransitions: StateTransitions<OrderStates, OrderEvents> = {
pending: { process: 'processing', cancel: 'cancelled' },
processing: { ship: 'shipped', cancel: 'cancelled' },
shipped: { deliver: 'delivered' },
delivered: {},
cancelled: {}
}
const orderActions: StateActions<OrderStates, OrderContext> = {
processing: {
onEnter: async (context) => {
console.log(`Processing order ${context.orderId}`)
// Charge payment, reserve inventory, etc.
}
},
shipped: {
onEnter: async (context) => {
console.log(`Order ${context.orderId} shipped`)
// Generate tracking number, send notification
context.trackingNumber = `TRK${Date.now()}`
}
},
cancelled: {
onEnter: async (context) => {
console.log(`Order ${context.orderId} cancelled`)
// Refund payment, release inventory
}
}
}
// Usage
const orderMachine = new StateMachine(
'pending',
{
orderId: 'ORD-001',
customerId: 'CUST-001',
items: [{ id: 'ITEM-001', quantity: 2 }],
total: 99.99
},
orderTransitions,
orderActions
)
await orderMachine.transition('process') // pending -> processing
await orderMachine.transition('ship') // processing -> shipped
Repository Pattern with Generic Constraints
// Generic repository pattern with type constraints
interface Entity {
id: string | number
createdAt: Date
updatedAt: Date
}
interface Repository<T extends Entity> {
findById(id: T['id']): Promise<T | null>
findAll(options?: QueryOptions<T>): Promise<T[]>
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>
update(id: T['id'], data: Partial<Omit<T, 'id' | 'createdAt'>>): Promise<T>
delete(id: T['id']): Promise<boolean>
}
interface QueryOptions<T> {
where?: Partial<T>
orderBy?: {
field: keyof T
direction: 'asc' | 'desc'
}
limit?: number
offset?: number
}
// Specific entity implementations
interface User extends Entity {
id: string
email: string
name: string
role: 'admin' | 'user'
}
interface Post extends Entity {
id: number
title: string
content: string
authorId: string
published: boolean
}
class BaseRepository<T extends Entity> implements Repository<T> {
constructor(private tableName: string) {}
async findById(id: T['id']): Promise<T | null> {
// Database implementation
return null
}
async findAll(options: QueryOptions<T> = {}): Promise<T[]> {
// Database implementation with type-safe queries
return []
}
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T> {
const now = new Date()
const entity = {
...data,
id: this.generateId(),
createdAt: now,
updatedAt: now,
} as T
// Save to database
return entity
}
async update(id: T['id'], data: Partial<Omit<T, 'id' | 'createdAt'>>): Promise<T> {
const updates = {
...data,
updatedAt: new Date(),
}
// Update in database
return {} as T
}
async delete(id: T['id']): Promise<boolean> {
// Delete from database
return true
}
private generateId(): T['id'] {
return Math.random().toString(36) as T['id']
}
}
// Specialized repositories with additional methods
class UserRepository extends BaseRepository<User> {
constructor() {
super('users')
}
async findByEmail(email: string): Promise<User | null> {
return this.findAll({ where: { email } }).then(users => users[0] || null)
}
async findAdmins(): Promise<User[]> {
return this.findAll({ where: { role: 'admin' } })
}
}
class PostRepository extends BaseRepository<Post> {
constructor() {
super('posts')
}
async findByAuthor(authorId: string): Promise<Post[]> {
return this.findAll({ where: { authorId } })
}
async findPublished(): Promise<Post[]> {
return this.findAll({ where: { published: true } })
}
}
Error Handling and Type Safety
Discriminated Unions for Error Handling
// Result type for error handling
type Result<T, E = Error> = Success<T> | Failure<E>
interface Success<T> {
success: true
data: T
}
interface Failure<E> {
success: false
error: E
}
// Helper functions for creating results
const success = <T>(data: T): Success<T> => ({ success: true, data })
const failure = <E>(error: E): Failure<E> => ({ success: false, error })
// Async result wrapper
class AsyncResult<T, E = Error> {
constructor(private promise: Promise<Result<T, E>>) {}
static async from<T>(
promise: Promise<T>
): Promise<Result<T, Error>> {
try {
const data = await promise
return success(data)
} catch (error) {
return failure(error instanceof Error ? error : new Error(String(error)))
}
}
async map<U>(fn: (data: T) => U): Promise<Result<U, E>> {
const result = await this.promise
if (result.success) {
return success(fn(result.data))
}
return result
}
async flatMap<U>(fn: (data: T) => Promise<Result<U, E>>): Promise<Result<U, E>> {
const result = await this.promise
if (result.success) {
return fn(result.data)
}
return result
}
async mapError<F>(fn: (error: E) => F): Promise<Result<T, F>> {
const result = await this.promise
if (!result.success) {
return failure(fn(result.error))
}
return result
}
async unwrap(): Promise<T> {
const result = await this.promise
if (result.success) {
return result.data
}
throw result.error
}
async unwrapOr(defaultValue: T): Promise<T> {
const result = await this.promise
return result.success ? result.data : defaultValue
}
}
// Usage example
interface ApiError {
code: string
message: string
statusCode: number
}
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return failure({
code: 'FETCH_ERROR',
message: 'Failed to fetch user',
statusCode: response.status
})
}
const user = await response.json()
return success(user)
} catch (error) {
return failure({
code: 'NETWORK_ERROR',
message: 'Network request failed',
statusCode: 0
})
}
}
// Chaining operations with error handling
async function getUserProfile(userId: string) {
const userResult = await fetchUser(userId)
if (!userResult.success) {
console.error('Failed to fetch user:', userResult.error.message)
return null
}
const user = userResult.data
// Continue with user data...
return user
}
Type Guards and Assertion Functions
// Advanced type guards
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 isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
// Generic type guard for object properties
function hasProperty<T extends Record<string, unknown>, K extends string>(
obj: T,
key: K
): obj is T & Record<K, unknown> {
return key in obj
}
// Assertion functions
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error(`Expected string, got ${typeof value}`)
}
}
function assertIsUser(value: unknown): asserts value is User {
if (!isObject(value)) {
throw new Error('Expected object')
}
if (!hasProperty(value, 'id') || !isString(value.id)) {
throw new Error('Expected id to be string')
}
if (!hasProperty(value, 'email') || !isString(value.email)) {
throw new Error('Expected email to be string')
}
if (!hasProperty(value, 'name') || !isString(value.name)) {
throw new Error('Expected name to be string')
}
}
// Runtime type validation with compile-time safety
interface ValidationSchema<T> {
[K in keyof T]: (value: unknown) => value is T[K]
}
function createValidator<T>(schema: ValidationSchema<T>) {
return (value: unknown): value is T => {
if (!isObject(value)) return false
for (const key in schema) {
if (!(key in value) || !schema[key](value[key])) {
return false
}
}
return true
}
}
const userValidator = createValidator<User>({
id: isString,
email: isString,
name: isString,
role: (value): value is 'admin' | 'user' =>
value === 'admin' || value === 'user',
createdAt: (value): value is Date => value instanceof Date,
updatedAt: (value): value is Date => value instanceof Date,
})
// Usage
function processUserData(data: unknown) {
if (userValidator(data)) {
// data is now typed as User
console.log(`Processing user: ${data.name}`)
} else {
throw new Error('Invalid user data')
}
}
Performance Considerations and Best Practices
Type-Only Imports and Exports
// Import only types (removed at runtime)
import type { User, Post } from './types'
import type { ApiResponse } from './api'
// Regular import for runtime values
import { validateUser, createPost } from './utils'
// Export types separately
export type { User, Post, ApiResponse }
export { validateUser, createPost }
// Conditional type imports
import type { ComponentProps } from 'react'
type ButtonProps = ComponentProps<'button'> & {
variant?: 'primary' | 'secondary'
}
Optimizing Complex Types
// Use interfaces for better performance with recursive types
interface TreeNode {
value: string
children: TreeNode[]
}
// Avoid deeply nested conditional types in hot paths
type SimpleConditional<T> = T extends string ? string : number
// Cache complex type computations
type CachedType<T> = T extends infer U ? SimpleConditional<U> : never
// Use branded types for performance and safety
type UserId = string & { readonly brand: unique symbol }
type PostId = number & { readonly brand: unique symbol }
function createUserId(id: string): UserId {
return id as UserId
}
function createPostId(id: number): PostId {
return id as PostId
}
// Type-safe but performant ID handling
function getUserPosts(userId: UserId): Promise<Post[]> {
// Implementation
return Promise.resolve([])
}
const userId = createUserId('user-123')
const posts = getUserPosts(userId) // Type-safe
Configuration and Environment Types
// Environment-specific configuration
interface BaseConfig {
apiUrl: string
version: string
}
interface DevelopmentConfig extends BaseConfig {
debug: true
mockApi: boolean
}
interface ProductionConfig extends BaseConfig {
debug: false
analytics: {
trackingId: string
enableCrashReporting: boolean
}
}
type Config = DevelopmentConfig | ProductionConfig
function createConfig(): Config {
if (process.env.NODE_ENV === 'development') {
return {
apiUrl: 'http://localhost:3000',
version: '1.0.0-dev',
debug: true,
mockApi: true,
}
}
return {
apiUrl: 'https://api.production.com',
version: '1.0.0',
debug: false,
analytics: {
trackingId: 'GA-XXXXXXX',
enableCrashReporting: true,
},
}
}
// Type-safe environment variable handling
interface EnvironmentVariables {
NODE_ENV: 'development' | 'production' | 'test'
API_URL: string
DATABASE_URL: string
JWT_SECRET: string
}
function getEnvVar<K extends keyof EnvironmentVariables>(
key: K
): EnvironmentVariables[K] {
const value = process.env[key]
if (!value) {
throw new Error(`Missing environment variable: ${key}`)
}
return value as EnvironmentVariables[K]
}
// Usage with full type safety
const apiUrl = getEnvVar('API_URL') // string
const nodeEnv = getEnvVar('NODE_ENV') // 'development' | 'production' | 'test'
Testing with TypeScript
Type-Safe Testing Utilities
// Test data factories with type safety
interface UserFactory {
id?: string
email?: string
name?: string
role?: 'admin' | 'user'
}
function createUser(overrides: UserFactory = {}): User {
return {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
role: 'user',
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
}
}
// Mock type generation
type MockedFunction<T extends (...args: any[]) => any> = jest.MockedFunction<T>
interface UserService {
findById(id: string): Promise<User | null>
create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User>
update(id: string, data: Partial<User>): Promise<User>
}
type MockedUserService = {
[K in keyof UserService]: MockedFunction<UserService[K]>
}
function createMockUserService(): MockedUserService {
return {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
}
}
// Type-safe test assertions
function expectType<T>(value: T): void {
// This function exists only for type checking
// It will be removed at runtime
}
describe('User Service', () => {
it('should return user with correct type', async () => {
const userService = createMockUserService()
const mockUser = createUser()
userService.findById.mockResolvedValue(mockUser)
const result = await userService.findById('user-123')
// Type assertion with compile-time checking
expectType<User | null>(result)
expect(result).toEqual(mockUser)
})
})
Conclusion
Mastering advanced TypeScript patterns transforms how you approach software development, enabling you to build applications that are not only more reliable and maintainable but also more expressive and self-documenting. The patterns and techniques covered in this guide represent the cutting edge of TypeScript development, from sophisticated type manipulations to architectural patterns that scale with your application's complexity.
The key to success with advanced TypeScript lies in understanding when to apply these patterns. Start with simpler approaches and gradually introduce more sophisticated techniques as your application's complexity grows. Remember that TypeScript's greatest strength is not in clever type gymnastics, but in creating clear, maintainable code that expresses your domain logic accurately and safely.
As TypeScript continues to evolve, these patterns will serve as a foundation for even more advanced techniques. The investment in mastering these concepts will pay dividends in reduced bugs, improved developer experience, and more confident refactoring capabilities.
Resources
- TypeScript Handbook
- Type Challenges
- TypeScript Deep Dive
- Effective TypeScript
- TypeScript Compiler API
Advanced TypeScript patterns are powerful tools that, when used correctly, can significantly improve your development experience and code quality. Practice these patterns in real projects to truly master the art of type-safe programming.
About Tridip Dutta
Creative Developer passionate about creating innovative digital experiences and exploring AI. I love sharing knowledge to help developers build better apps.