TypeScript

Advanced Types

28 min Lesson 17 of 40

Advanced Types in TypeScript

TypeScript offers a rich set of advanced type features that allow you to express complex type relationships and create highly type-safe code. In this lesson, we'll explore advanced type operators and patterns that will take your TypeScript skills to the next level.

The keyof Operator

The keyof operator takes an object type and produces a string or numeric literal union of its keys. This is incredibly useful for creating type-safe property access patterns.

TypeScript Code:
<interface Person {
  name: string;
  age: number;
  email: string;
}

// Create a union type of all keys
type PersonKeys = keyof Person;
// Result: 'name' | 'age' | 'email'

// Practical example: type-safe property getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person: Person = {
  name: 'John Doe',
  age: 30,
  email: 'john@example.com'
};

const name = getProperty(person, 'name'); // Type: string
const age = getProperty(person, 'age');   // Type: number

// Error: Argument of type 'invalid' is not assignable
// const invalid = getProperty(person, 'invalid');
>
Tip: keyof is essential for creating type-safe functions that work with object properties dynamically.

The typeof Operator

The typeof operator gets the type of a value. This is particularly useful when you want to reference the type of an existing value without explicitly defining a separate type.

TypeScript Code:
<// Get type from a value
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
  headers: {
    'Content-Type': 'application/json'
  }
};

type Config = typeof config;
// Result: {
//   apiUrl: string;
//   timeout: number;
//   retries: number;
//   headers: { 'Content-Type': string; };
// }

// Get function type
function createUser(name: string, email: string) {
  return { name, email, id: Math.random() };
}

type CreateUserFn = typeof createUser;
// Result: (name: string, email: string) => { name: string; email: string; id: number; }

// Combine with ReturnType
type User = ReturnType<typeof createUser>;
// Result: { name: string; email: string; id: number; }

// Get enum values
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT'
} as const;

type DirectionType = typeof Direction;
type DirectionValue = typeof Direction[keyof typeof Direction];
// Result: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
>

Indexed Access Types

Indexed access types allow you to look up specific properties on another type using bracket notation. This is useful for extracting the type of nested properties.

TypeScript Code:
<interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
    coordinates: {
      lat: number;
      lng: number;
    };
  };
  employees: Array<{
    name: string;
    role: string;
    salary: number;
  }>;
}

// Access nested property types
type Address = Company['address'];
// Result: { street: string; city: string; country: string; coordinates: {...}; }

type Coordinates = Company['address']['coordinates'];
// Result: { lat: number; lng: number; }

type EmployeeArray = Company['employees'];
// Result: Array<{ name: string; role: string; salary: number; }>

// Access array element type
type Employee = Company['employees'][number];
// Result: { name: string; role: string; salary: number; }

// Access specific property of array element
type EmployeeRole = Company['employees'][number]['role'];
// Result: string

// Multiple indexed access
type CityOrCountry = Company['address']['city' | 'country'];
// Result: string
>
Note: Use [number] to access the element type of an array type.

Template Literal Types

Template literal types build on string literal types and can expand into many strings via unions. They allow you to create new string types based on existing ones.

TypeScript Code:
<// Basic template literal type
type World = 'world';
type Greeting = `hello ${World}`;
// Result: 'hello world'

// Create API endpoint types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'user' | 'post' | 'comment';
type Endpoint = `/${Lowercase<Resource>}`;
// Result: '/user' | '/post' | '/comment'

type ApiRoute = `${HttpMethod} ${Endpoint}`;
// Result: 'GET /user' | 'GET /post' | ... (12 combinations)

// Event names
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// Result: 'onClick' | 'onFocus' | 'onBlur'

// CSS properties
type CSSProperty = 'color' | 'font-size' | 'border-width';
type CSSValue = string | number;
type StyleObject = {
  [K in CSSProperty]: CSSValue;
};

// Advanced: Create getter/setter types
type PropName = 'name' | 'age' | 'email';
type Getters = {
  [K in PropName as `get${Capitalize<K>}`]: () => string;
};
// Result: { getName: () => string; getAge: () => string; getEmail: () => string; }

type Setters = {
  [K in PropName as `set${Capitalize<K>}`]: (value: string) => void;
};
>
Tip: Template literal types are perfect for generating type-safe API routes, event handlers, and property names.

Intrinsic String Manipulation Types

TypeScript provides built-in types for common string manipulations: Uppercase, Lowercase, Capitalize, and Uncapitalize.

TypeScript Code:
<type Greeting = 'hello world';

// Transform to uppercase
type LoudGreeting = Uppercase<Greeting>;
// Result: 'HELLO WORLD'

// Transform to lowercase
type QuietGreeting = Lowercase<LoudGreeting>;
// Result: 'hello world'

// Capitalize first letter
type CapitalizedGreeting = Capitalize<Greeting>;
// Result: 'Hello world'

// Uncapitalize first letter
type UncapitalizedGreeting = Uncapitalize<CapitalizedGreeting>;
// Result: 'hello world'

// Practical example: HTTP headers
type HttpHeader = 'content-type' | 'authorization' | 'accept';
type HttpHeaderCapitalized = Capitalize<HttpHeader>;
// Result: 'Content-type' | 'Authorization' | 'Accept'

// Create constants
type StatusCode = 'ok' | 'error' | 'pending';
type StatusConstant = `STATUS_${Uppercase<StatusCode>}`;
// Result: 'STATUS_OK' | 'STATUS_ERROR' | 'STATUS_PENDING'
>

Recursive Types

Recursive types reference themselves in their definition. This is powerful for modeling nested or tree-like structures.

TypeScript Code:
<// Simple recursive type: JSON
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

const validJSON: JSONValue = {
  name: 'John',
  age: 30,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'New York',
    coordinates: {
      lat: 40.7128,
      lng: -74.0060
    }
  }
};

// File system tree
interface FileSystemNode {
  name: string;
  type: 'file' | 'directory';
  children?: FileSystemNode[];
}

const fileSystem: FileSystemNode = {
  name: 'root',
  type: 'directory',
  children: [
    {
      name: 'src',
      type: 'directory',
      children: [
        { name: 'index.ts', type: 'file' },
        { name: 'utils.ts', type: 'file' }
      ]
    },
    { name: 'package.json', type: 'file' }
  ]
};

// Deeply nested object paths
type PathImpl<T, Key extends keyof T> =
  Key extends string
    ? T[Key] extends Record<string, any>
      ? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
      | `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
      : never
    : never;

type Path<T> = PathImpl<T, keyof T> | keyof T;

interface DeepObject {
  user: {
    profile: {
      name: string;
      settings: {
        theme: string;
      };
    };
  };
}

type DeepPaths = Path<DeepObject>;
// Result: 'user' | 'user.profile' | 'user.profile.name' | 'user.profile.settings' | ...
>
Warning: TypeScript has a recursion depth limit. Very deep recursive types may cause compilation errors.

Mapped Types with Key Remapping

Key remapping allows you to transform keys in mapped types using the as clause. This enables sophisticated type transformations.

TypeScript Code:
<interface Person {
  name: string;
  age: number;
  email: string;
}

// Create getters with key remapping
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

// Filter properties by type
type FilterByType<T, ValueType> = {
  [K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};

type StringPropsOnly = FilterByType<Person, string>;
// Result: { name: string; email: string; }

type NumberPropsOnly = FilterByType<Person, number>;
// Result: { age: number; }

// Remove specific prefixes
type RemovePrefix<T, Prefix extends string> = {
  [K in keyof T as K extends `${Prefix}${infer Rest}` ? Rest : K]: T[K];
};

interface PrefixedData {
  data_name: string;
  data_age: number;
  email: string;
}

type CleanedData = RemovePrefix<PrefixedData, 'data_'>;
// Result: { name: string; age: number; email: string; }
>

Conditional Types with Inference

Conditional types can infer types within the condition using the infer keyword. This enables powerful type extraction patterns.

TypeScript Code:
<// Extract return type
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: 1, name: 'John' };
}

type UserType = GetReturnType<typeof getUser>;
// Result: { id: number; name: string; }

// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;

type Numbers = ArrayElement<number[]>;
// Result: number

type Users = ArrayElement<Array<{ id: number; name: string }>>;
// Result: { id: number; name: string; }

// Extract Promise value type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type AsyncResult = UnwrapPromise<Promise<string>>;
// Result: string

type SyncResult = UnwrapPromise<number>;
// Result: number

// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function processUser(id: number, name: string, active: boolean) {
  // ...
}

type ProcessUserParams = Parameters<typeof processUser>;
// Result: [id: number, name: string, active: boolean]

// Extract first parameter
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

type FirstParam = FirstParameter<typeof processUser>;
// Result: number
>
Note: The infer keyword can only be used within the extends clause of a conditional type.

Distributive Conditional Types

When conditional types act on a generic type, they become distributive when given a union type. This means the conditional type is applied to each member of the union.

TypeScript Code:
<// Simple distributive example
type ToArray<T> = T extends any ? T[] : never;

type StringOrNumberArray = ToArray<string | number>;
// Result: string[] | number[] (not (string | number)[])

// Filter null from union
type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Result: string

// Extract types matching a pattern
type ExtractStrings<T> = T extends string ? T : never;

type Mixed = string | number | boolean | 'specific';
type OnlyStrings = ExtractStrings<Mixed>;
// Result: string | 'specific'

// Prevent distribution with tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type CombinedArray = ToArrayNonDist<string | number>;
// Result: (string | number)[]
>

Practical Example: Type-Safe Event Emitter

Complete Example:
<// Define event map
interface EventMap {
  'user:login': { userId: number; timestamp: Date };
  'user:logout': { userId: number };
  'data:update': { id: string; data: any };
  'error': { message: string; code: number };
}

// Extract event names
type EventName = keyof EventMap;

// Create type-safe event emitter
class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: Array<(payload: T[K]) => void>;
  } = {};

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    const eventListeners = this.listeners[event];
    if (eventListeners) {
      eventListeners.forEach(listener => listener(payload));
    }
  }

  off<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    const eventListeners = this.listeners[event];
    if (eventListeners) {
      this.listeners[event] = eventListeners.filter(l => l !== listener) as any;
    }
  }
}

// Usage
const emitter = new TypedEventEmitter<EventMap>();

// Type-safe: payload is inferred correctly
emitter.on('user:login', (payload) => {
  console.log(`User ${payload.userId} logged in at ${payload.timestamp}`);
});

// Type-safe: must provide correct payload
emitter.emit('user:login', {
  userId: 123,
  timestamp: new Date()
});

// Error: Wrong payload type
// emitter.emit('user:login', { userId: '123' }); // Error: string not assignable to number

// Error: Unknown event
// emitter.on('invalid:event', () => {}); // Error: invalid:event not in EventMap
>
Exercise: Create a type that:
  1. Takes an interface with various property types
  2. Creates a new type with only the string properties
  3. Transforms each property name to have a 'validate' prefix
  4. Changes each property type to a validation function returning boolean
Summary: Advanced types in TypeScript enable you to create sophisticated type relationships and transformations. Master these patterns to write highly type-safe and maintainable code.