TypeScript Advanced Types: From Beginner to Expert

Master TypeScript advanced type techniques and turn the type system into your development superpower

TypeScript Advanced Types: From Beginner to Expert

TypeScript’s type system is far more powerful than just string, number, and boolean. By mastering advanced type techniques, you can build code that is both type-safe and elegantly flexible. This article takes you deep into the essence of TypeScript’s type system.

The Art of Type Inference

as const Assertion

// Regular declaration: type is widened
const config = {
  endpoint: '/api/users',
  method: 'GET'
};
// Type: { endpoint: string; method: string }

// as const: type is narrowed to literals
const config = {
  endpoint: '/api/users',
  method: 'GET'
} as const;
// Type: { readonly endpoint: "/api/users"; readonly method: "GET" }

as const is extremely useful for defining constant configurations and enum alternatives:

const HttpMethods = ['GET', 'POST', 'PUT', 'DELETE'] as const;
type HttpMethod = typeof HttpMethods[number];
// Type: "GET" | "POST" | "PUT" | "DELETE"

The satisfies Operator

TypeScript 4.9 introduced satisfies, giving you both type checking and precise inference:

type Colors = Record<string, string | number[]>;

// Using type annotation: loses specific info
const colors: Colors = {
  red: '#ff0000',
  green: [0, 255, 0]
};
colors.red.toUpperCase(); // ❌ Error: might be number[]

// Using satisfies: preserves specific types
const colors = {
  red: '#ff0000',
  green: [0, 255, 0]
} satisfies Colors;
colors.red.toUpperCase();   // ✅ OK: knows it's string
colors.green.map(n => n);   // ✅ OK: knows it's number[]

Conditional Types

Conditional types are the core of TypeScript type programming:

type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>;  // true
type B = IsString<42>;       // false

The infer Keyword

infer lets you extract types within conditional types:

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

type Fn = () => Promise<string>;
type Result = ReturnType<Fn>;  // Promise<string>

// Extract Promise inner type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type A = Awaited<Promise<Promise<string>>>;  // string

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

type E = ElementType<string[]>;  // string

Practical Conditional Types

// Extract keys whose values are functions
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

interface User {
  name: string;
  age: number;
  greet(): void;
  update(data: Partial<User>): void;
}

type UserMethods = FunctionKeys<User>;  // "greet" | "update"

Mapped Types

Mapped types let you create new types based on existing ones:

// Basic mapping
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
};

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

type Required<T> = {
  [K in keyof T]-?: T[K]  // -? removes optional marker
};

Key Remapping

TypeScript 4.1 introduced the as clause for key remapping:

// Add prefix to all keys
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]
};

interface User {
  name: string;
  age: number;
}

type PrefixedUser = Prefixed<User, 'user'>;
// { userName: string; userAge: number }

// Filter keys of specific types
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
};

type StringProps = OnlyStrings<User>;
// { name: string }

Getter/Setter Generator

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

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};

interface Person {
  name: string;
  age: number;
}

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

type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }

Template Literal Types

TypeScript 4.1 brought powerful template literal types:

type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>;  // "onClick"
type FocusEvent = EventName<'focus'>;  // "onFocus"

// Combining multiple literals
type Vertical = 'top' | 'bottom';
type Horizontal = 'left' | 'right';
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"

Built-in String Manipulation Types

type Upper = Uppercase<'hello'>;      // "HELLO"
type Lower = Lowercase<'HELLO'>;      // "hello"
type Cap = Capitalize<'hello'>;       // "Hello"
type Uncap = Uncapitalize<'Hello'>;   // "hello"

Practical: Type-Safe Event System

type EventMap = {
  click: { x: number; y: number };
  focus: { target: HTMLElement };
  submit: { data: FormData };
};

type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;

type EventHandlers = {
  [K in keyof EventMap as `on${Capitalize<K>}`]: EventHandler<K>
};

// Result:
// {
//   onClick: (event: { x: number; y: number }) => void;
//   onFocus: (event: { target: HTMLElement }) => void;
//   onSubmit: (event: { data: FormData }) => void;
// }

Recursive Types

TypeScript supports recursive type definitions:

// Deep readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K]
};

// Deep partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? DeepPartial<T[K]>
    : T[K]
};

// Flatten nested object paths
type Paths<T, D extends number = 10> = [D] extends [never]
  ? never
  : T extends object
    ? {
        [K in keyof T]-?: K extends string | number
          ? `${K}` | `${K}.${Paths<T[K], Prev[D]>}`
          : never
      }[keyof T]
    : never;

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

interface Config {
  server: {
    host: string;
    port: number;
  };
  database: {
    connection: {
      url: string;
    };
  };
}

type ConfigPaths = Paths<Config>;
// "server" | "server.host" | "server.port" | "database" | "database.connection" | "database.connection.url"

Type Gymnastics in Practice

1. Tuple Operations

// Get first element of tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

// Get last element of tuple
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

// Remove first element
type Shift<T extends any[]> = T extends [any, ...infer R] ? R : never;

// Remove last element
type Pop<T extends any[]> = T extends [...infer R, any] ? R : never;

// Reverse tuple
type Reverse<T extends any[]> = T extends [infer F, ...infer R]
  ? [...Reverse<R>, F]
  : [];

type Tuple = [1, 2, 3, 4, 5];
type FirstEl = First<Tuple>;     // 1
type LastEl = Last<Tuple>;       // 5
type Shifted = Shift<Tuple>;     // [2, 3, 4, 5]
type Popped = Pop<Tuple>;        // [1, 2, 3, 4]
type Reversed = Reverse<Tuple>;  // [5, 4, 3, 2, 1]

2. Union to Intersection

type UnionToIntersection<U> =
  (U extends any ? (x: U) => void : never) extends
  (x: infer I) => void ? I : never;

type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }

3. Strict Object Types

// Exact type, no extra properties allowed
type Exact<T, Shape> = T extends Shape
  ? Exclude<keyof T, keyof Shape> extends never
    ? T
    : never
  : never;

type User = { name: string; age: number };

function createUser<T extends User>(user: Exact<T, User>): User {
  return user;
}

createUser({ name: 'Alice', age: 30 });           // ✅
createUser({ name: 'Bob', age: 25, extra: 1 });   // ❌ Type error

Utility Types

Deep Required

type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object
    ? DeepRequired<T[K]>
    : T[K]
};

Nullable Type Handling

type NonNullableDeep<T> = {
  [K in keyof T]: NonNullable<T[K]> extends object
    ? NonNullableDeep<NonNullable<T[K]>>
    : NonNullable<T[K]>
};

Selective Pick and Omit

// Pick only properties of specific type
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
};

// Omit properties of specific type
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K]
};

interface Mixed {
  name: string;
  age: number;
  active: boolean;
  email: string;
}

type StringProps = PickByType<Mixed, string>;
// { name: string; email: string }

type NonBooleanProps = OmitByType<Mixed, boolean>;
// { name: string; age: number; email: string }

Type Assertion Functions

// Assertion function
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Not a string');
  }
}

function processValue(value: unknown) {
  assertIsString(value);
  // value is now typed as string
  console.log(value.toUpperCase());
}

// Type guard
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'name' in obj &&
    'age' in obj
  );
}

function greet(input: unknown) {
  if (isUser(input)) {
    // input is typed as User
    console.log(`Hello, ${input.name}!`);
  }
}

Best Practices

1. Avoid Overusing any

// ❌ Avoid
function process(data: any): any {
  return data.value;
}

// ✅ Use generics
function process<T extends { value: unknown }>(data: T): T['value'] {
  return data.value;
}

2. Prefer unknown Over any

// ❌ any allows any operation
function dangerous(value: any) {
  value.foo.bar.baz();  // No error, but might crash at runtime
}

// ✅ unknown forces type checking
function safe(value: unknown) {
  if (typeof value === 'object' && value !== null && 'foo' in value) {
    // Safe access
  }
}

3. Leverage const Generics

function createConfig<const T extends Record<string, unknown>>(config: T): T {
  return config;
}

const config = createConfig({
  api: '/api',
  timeout: 5000
});
// Type: { readonly api: "/api"; readonly timeout: 5000 }

Summary

TypeScript advanced types are a double-edged sword:

ScenarioRecommendation
Library/Framework devFully leverage advanced types for best DX
Business codeUse moderately, maintain readability
Type gymnasticsLearn principles, use cautiously in production

Key Takeaways:

  1. as const and satisfies improve type precision
  2. Conditional types + infer are the core of type programming
  3. Mapped types + key remapping enable type transformations
  4. Template literal types handle string types
  5. Recursive types handle nested structures

Master these advanced type techniques, and your TypeScript code will be more type-safe and expressive. Remember: the type system is your friend, not your enemy.


Types are documentation, types are tests, types are your safety net. Let TypeScript’s type system guard your code.