Skip to content

Advanced TypeScript Tutorial

This tutorial will explore advanced TypeScript features and best practices to help you become a TypeScript expert.

Advanced Types

Union and Intersection Types

typescript
// Union types
type Status = 'loading' | 'success' | 'error'
type ID = string | number

// Intersection types
interface User {
  name: string
  email: string
}

interface Admin {
  permissions: string[]
}

type AdminUser = User & Admin

const adminUser: AdminUser = {
  name: 'John',
  email: 'john@example.com',
  permissions: ['read', 'write', 'delete']
}

Conditional Types

typescript
// Basic conditional types
type IsString<T> = T extends string ? true : false

type Test1 = IsString<string>  // true
type Test2 = IsString<number>  // false

// Useful conditional types
type NonNullable<T> = T extends null | undefined ? never : T
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any

// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never
type StrArrOrNumArr = ToArray<string | number>  // string[] | number[]

Mapped Types

typescript
// Basic mapped types
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

type Partial<T> = {
  [P in keyof T]?: T[P]
}

// Custom mapped types
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person {
  name: string
  age: number
}

type PersonGetters = Getters<Person>
// {
//   getName: () => string
//   getAge: () => number
// }

Template Literal Types

typescript
// Basic template literal types
type Greeting = `Hello ${string}`
type EmailLocaleIDs = `${string}_${string}`

// Combined with mapped types
type EventNames<T extends string> = `${T}Changed`
type PersonEvents = EventNames<'name' | 'age'>  // 'nameChanged' | 'ageChanged'

// Practical example: CSS properties
type CSSProperties = {
  [K in 
    | 'color' 
    | 'background-color' 
    | 'font-size'
    as `--${K}`
  ]?: string
}

const styles: CSSProperties = {
  '--color': 'red',
  '--background-color': 'blue',
  '--font-size': '16px'
}

Advanced Generics

Generic Constraints

typescript
// Basic constraints
interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

// Using type parameters to constrain
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const person = { name: 'John', age: 30 }
const name = getProperty(person, 'name')  // string
const age = getProperty(person, 'age')    // number

Generic Utility Types

typescript
// Custom utility types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

// Function overloads with generics
function createElement<T extends keyof HTMLElementTagNameMap>(
  tag: T
): HTMLElementTagNameMap[T]
function createElement<T extends HTMLElement>(
  tag: string
): T
function createElement(tag: string): HTMLElement {
  return document.createElement(tag)
}

const div = createElement('div')      // HTMLDivElement
const span = createElement('span')    // HTMLSpanElement
const custom = createElement<HTMLElement>('custom')  // HTMLElement

Decorators

Class Decorators

typescript
// Class decorator
function sealed(constructor: Function) {
  Object.seal(constructor)
  Object.seal(constructor.prototype)
}

function classDecorator<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  return class extends constructor {
    newProperty = 'new property'
    hello = 'override'
  }
}

@sealed
@classDecorator
class BugReport {
  type = 'report'
  title: string

  constructor(t: string) {
    this.title = t
  }
}

Method Decorators

typescript
// Method decorator
function enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value
  }
}

function log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args)
    const result = originalMethod.apply(this, args)
    console.log(`Result:`, result)
    return result
  }
}

class Calculator {
  @enumerable(false)
  @log
  add(a: number, b: number): number {
    return a + b
  }
}

Property Decorators

typescript
// Property decorator
function format(formatString: string) {
  return function (target: any, propertyKey: string) {
    let value: string

    const getter = () => value
    const setter = (newVal: string) => {
      value = formatString.replace('%s', newVal)
    }

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    })
  }
}

class Greeter {
  @format('Hello, %s!')
  greeting: string

  constructor(message: string) {
    this.greeting = message
  }
}

const greeter = new Greeter('world')
console.log(greeter.greeting)  // "Hello, world!"

Module System

Namespaces

typescript
// Namespace
namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean
  }

  const lettersRegexp = /^[A-Za-z]+$/
  const numberRegexp = /^[0-9]+$/

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s)
    }
  }

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s)
    }
  }
}

// Using namespaces
const validators: { [s: string]: Validation.StringValidator } = {}
validators['ZIP code'] = new Validation.ZipCodeValidator()
validators['Letters only'] = new Validation.LettersOnlyValidator()

Module Declarations

typescript
// Declare external modules
declare module 'lodash' {
  export function chunk<T>(array: T[], size?: number): T[][]
  export function debounce<T extends (...args: any[]) => any>(
    func: T,
    wait?: number,
    options?: object
  ): T
}

// Global declarations
declare global {
  interface Window {
    myGlobalFunction: (name: string) => void
  }
}

// Module augmentation
declare module 'vue' {
  interface ComponentCustomProperties {
    $myGlobalProperty: string
  }
}

Practical Tips

Type Guards

typescript
// User-defined type guards
function isString(value: any): value is string {
  return typeof value === 'string'
}

function isNumber(value: any): value is number {
  return typeof value === 'number'
}

// Using type guards
function processValue(value: string | number) {
  if (isString(value)) {
    // value type is narrowed to string
    console.log(value.toUpperCase())
  } else if (isNumber(value)) {
    // value type is narrowed to number
    console.log(value.toFixed(2))
  }
}

// Discriminated unions
interface Bird {
  type: 'bird'
  flyingSpeed: number
}

interface Horse {
  type: 'horse'
  runningSpeed: number
}

type Animal = Bird | Horse

function moveAnimal(animal: Animal) {
  switch (animal.type) {
    case 'bird':
      console.log('Flying at speed: ' + animal.flyingSpeed)
      break
    case 'horse':
      console.log('Running at speed: ' + animal.runningSpeed)
      break
  }
}

Index Signatures and Mapped Types

typescript
// Index signatures
interface StringDictionary {
  [key: string]: string
}

interface NumberDictionary {
  [key: string]: number
  length: number    // ok, length is a number
  name: string      // error, name's type doesn't match the index signature
}

// Advanced mapped types
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

interface User {
  id: number
  name: string
  email: string
  password: string
}

type PublicUser = Omit<User, 'password'>  // { id: number; name: string; email: string }
type UserCredentials = Pick<User, 'email' | 'password'>  // { email: string; password: string }

Function Overloads

typescript
// Function overloads
function reverse(x: string): string
function reverse(x: number): number
function reverse(x: boolean): boolean
function reverse(x: string | number | boolean): string | number | boolean {
  if (typeof x === 'string') {
    return x.split('').reverse().join('')
  } else if (typeof x === 'number') {
    return Number(x.toString().split('').reverse().join(''))
  } else {
    return !x
  }
}

const reversedString = reverse('hello')    // string
const reversedNumber = reverse(12345)      // number
const reversedBoolean = reverse(true)      // boolean

Real-world Project: Type-safe API Client

Let's create a type-safe API client:

typescript
// types/api.ts
export interface User {
  id: number
  name: string
  email: string
  createdAt: string
}

export interface CreateUserRequest {
  name: string
  email: string
}

export interface UpdateUserRequest {
  name?: string
  email?: string
}

export interface ApiResponse<T> {
  data: T
  message: string
  success: boolean
}

export interface PaginatedResponse<T> {
  data: T[]
  total: number
  page: number
  limit: number
}

// api/client.ts
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

interface RequestConfig {
  method: HttpMethod
  headers?: Record<string, string>
  body?: any
}

class ApiClient {
  private baseURL: string
  private defaultHeaders: Record<string, string>

  constructor(baseURL: string) {
    this.baseURL = baseURL
    this.defaultHeaders = {
      'Content-Type': 'application/json'
    }
  }

  private async request<T>(
    endpoint: string, 
    config: RequestConfig
  ): Promise<ApiResponse<T>> {
    const url = `${this.baseURL}${endpoint}`
    const headers = { ...this.defaultHeaders, ...config.headers }

    const response = await fetch(url, {
      method: config.method,
      headers,
      body: config.body ? JSON.stringify(config.body) : undefined
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    return response.json()
  }

  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, { method: 'GET' })
  }

  async post<T, U = any>(
    endpoint: string, 
    data: U
  ): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: data
    })
  }

  async put<T, U = any>(
    endpoint: string, 
    data: U
  ): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: data
    })
  }

  async delete<T>(endpoint: string): Promise<ApiResponse<T>> {
    return this.request<T>(endpoint, { method: 'DELETE' })
  }
}

// api/users.ts
export class UserService {
  constructor(private client: ApiClient) {}

  async getUsers(): Promise<ApiResponse<PaginatedResponse<User>>> {
    return this.client.get<PaginatedResponse<User>>('/users')
  }

  async getUser(id: number): Promise<ApiResponse<User>> {
    return this.client.get<User>(`/users/${id}`)
  }

  async createUser(data: CreateUserRequest): Promise<ApiResponse<User>> {
    return this.client.post<User, CreateUserRequest>('/users', data)
  }

  async updateUser(
    id: number, 
    data: UpdateUserRequest
  ): Promise<ApiResponse<User>> {
    return this.client.put<User, UpdateUserRequest>(`/users/${id}`, data)
  }

  async deleteUser(id: number): Promise<ApiResponse<void>> {
    return this.client.delete<void>(`/users/${id}`)
  }
}

// Usage example
const apiClient = new ApiClient('https://api.example.com')
const userService = new UserService(apiClient)

async function example() {
  try {
    // Get user list
    const usersResponse = await userService.getUsers()
    console.log(usersResponse.data.data) // User[]

    // Create new user
    const newUser = await userService.createUser({
      name: 'John Doe',
      email: 'john@example.com'
    })
    console.log(newUser.data) // User

    // Update user
    const updatedUser = await userService.updateUser(1, {
      name: 'Jane Doe'
    })
    console.log(updatedUser.data) // User

  } catch (error) {
    console.error('API Error:', error)
  }
}

Performance Optimization

Type Inference Optimization

typescript
// Avoid excessive annotations
// ❌ Bad
const users: User[] = await getUsers()

// ✅ Good
const users = await getUsers() // TypeScript can infer the type

// Use const assertions
// ❌ Bad
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
} // Type is { apiUrl: string; timeout: number }

// ✅ Good
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
} as const // Type is { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }

Compilation Optimization

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "skipLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Testing

Type Testing

typescript
// Type testing utilities
type Expect<T extends true> = T
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
  ? true
  : false

// Test cases
type cases = [
  Expect<Equal<Pick<User, 'name'>, { name: string }>>,
  Expect<Equal<Omit<User, 'id'>, { name: string; email: string; createdAt: string }>>,
]

// Runtime testing
import { describe, it, expect } from 'vitest'

describe('UserService', () => {
  it('should create user with correct type', async () => {
    const userService = new UserService(apiClient)
    const userData: CreateUserRequest = {
      name: 'Test User',
      email: 'test@example.com'
    }
    
    const response = await userService.createUser(userData)
    
    expect(response.data).toHaveProperty('id')
    expect(response.data).toHaveProperty('name', userData.name)
    expect(response.data).toHaveProperty('email', userData.email)
  })
})

Best Practices

1. Type Design Principles

typescript
// ✅ Use union types instead of enums
type Theme = 'light' | 'dark' | 'auto'

// ✅ Use interface extension
interface BaseEntity {
  id: string
  createdAt: Date
  updatedAt: Date
}

interface User extends BaseEntity {
  name: string
  email: string
}

// ✅ Use generic constraints
function processItems<T extends { id: string }>(items: T[]): T[] {
  return items.filter(item => item.id.length > 0)
}

2. Error Handling

typescript
// Result type pattern
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E }

async function safeApiCall<T>(
  apiCall: () => Promise<T>
): Promise<Result<T>> {
  try {
    const data = await apiCall()
    return { success: true, data }
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error : new Error(String(error))
    }
  }
}

// Usage
const result = await safeApiCall(() => userService.getUser(1))
if (result.success) {
  console.log(result.data) // User
} else {
  console.error(result.error) // Error
}

3. Configuration Management

typescript
// Environment configuration
interface Config {
  readonly apiUrl: string
  readonly timeout: number
  readonly retries: number
  readonly debug: boolean
}

const createConfig = (env: string): Config => {
  const baseConfig = {
    timeout: 5000,
    retries: 3,
    debug: false
  }

  switch (env) {
    case 'development':
      return {
        ...baseConfig,
        apiUrl: 'http://localhost:3000',
        debug: true
      }
    case 'production':
      return {
        ...baseConfig,
        apiUrl: 'https://api.production.com'
      }
    default:
      throw new Error(`Unknown environment: ${env}`)
  }
}

export const config = createConfig(process.env.NODE_ENV || 'development')

Summary

Advanced TypeScript features allow us to:

  1. Type Safety - Catch errors at compile time
  2. Code Completion - Better development experience
  3. Refactoring Support - Safely refactor code
  4. Documentation - Types serve as documentation
  5. Team Collaboration - Unified code standards

Next Steps

  1. TypeScript with React
  2. TypeScript with Node.js
  3. TypeScript Compiler API
  4. TypeScript Performance Optimization

References


Master these advanced TypeScript features to write safer, more elegant code! 🚀

VitePress Development Guide