Intermediate80 min read

Type Aliases and Advanced Type Declarations

Master TypeScript type aliases, union types, intersection types, and advanced type declaration patterns for flexible and powerful type definitions.

Topics Covered:

Type AliasesUnion TypesIntersection TypesLiteral TypesTemplate Literal TypesMapped TypesConditional Types

Prerequisites:

  • TypeScript Basics for React Developers
  • TypeScript Interfaces: Comprehensive Guide

Video Tutorial

Overview

Type aliases provide a way to create new names for types, making complex types reusable and readable. Combined with union types, intersection types, and advanced TypeScript features, type aliases enable powerful type declarations. This tutorial covers everything from basic type aliases to advanced patterns like conditional types and template literal types, giving you the tools to create flexible and expressive type systems.

Understanding Type Aliases

Type aliases create new names for types. They're similar to interfaces but more flexible, supporting unions, intersections, and other complex types. What are Type Aliases: • New names for existing types • Created with type keyword • Can represent any type • More flexible than interfaces • Cannot be merged (unlike interfaces) When to Use Type Aliases: • Union types • Intersection types • Complex type combinations • Primitive type aliases • Function types • Tuple types Type Alias vs Interface: • Interfaces: Object shapes, can be merged, extended • Type aliases: Any type, cannot be merged, more flexible • Use interfaces for object shapes • Use type aliases for unions, intersections, primitives

Code Example:
// Basic type alias
type UserID = string;
type Age = number;
type IsActive = boolean;

// Using type aliases
const userId: UserID = "user-123";
const age: Age = 30;
const isActive: IsActive = true;

// Type alias for object (similar to interface)
type User = {
  id: UserID;
  name: string;
  age: Age;
  isActive: IsActive;
};

// Type alias for function
type GreetFunction = (name: string) => string;

const greet: GreetFunction = (name) => `Hello, ${name}!`;

// Type alias for array
type StringArray = string[];
type NumberList = number[];

const names: StringArray = ["Alice", "Bob"];
const numbers: NumberList = [1, 2, 3];

// Type alias for tuple
type Coordinate = [number, number];
type RGB = [number, number, number];

const point: Coordinate = [10, 20];
const color: RGB = [255, 0, 0];

// Type alias for React component props
type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

function Button({ label, onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {label}
    </button>
  );
}

// Type alias vs Interface
// Interface (can be extended and merged)
interface IUser {
  name: string;
}

// Type alias (cannot be merged)
type TUser = {
  name: string;
};

// Both work similarly, but interfaces have merging capability

Type aliases create reusable names for types. Use them for any type, especially unions, intersections, and complex combinations. They're more flexible than interfaces but cannot be merged.

Union Types

Union types allow a value to be one of several types. They're created using the | operator and are one of TypeScript's most powerful features. What are Union Types: • Value can be one of multiple types • Created with | operator • TypeScript narrows type based on usage • Essential for flexible APIs • Enable type-safe alternatives Use Cases: • Function parameters that accept multiple types • API responses with different shapes • Component props with variants • State that can be different types • Error handling Type Narrowing: • TypeScript narrows union types • Use typeof, instanceof, or type guards • Enables type-safe code • Prevents runtime errors

Code Example:
// Basic union type
type StringOrNumber = string | number;

function processValue(value: StringOrNumber) {
  // TypeScript knows value is string OR number
  if (typeof value === "string") {
    // TypeScript narrows to string here
    return value.toUpperCase();
  } else {
    // TypeScript narrows to number here
    return value * 2;
  }
}

// Union of literal types
type Status = "pending" | "success" | "error";
type Theme = "light" | "dark";

function setStatus(status: Status) {
  console.log(`Status: ${status}`);
}

setStatus("pending"); // ✅ OK
setStatus("success"); // ✅ OK
setStatus("invalid"); // ❌ Error

// Union with null/undefined
type MaybeString = string | null | undefined;

function getValue(): MaybeString {
  return Math.random() > 0.5 ? "value" : null;
}

// React component with union props
type ButtonVariant = "primary" | "secondary" | "outline";
type ButtonSize = "sm" | "md" | "lg";

type ButtonProps = {
  variant: ButtonVariant;
  size: ButtonSize;
  onClick: () => void;
};

function Button({ variant, size, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
    >
      Click me
    </button>
  );
}

// Union for API responses
type ApiResponse = 
  | { status: "success"; data: User }
  | { status: "error"; message: string };

function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    // TypeScript knows response.data exists
    console.log(response.data);
  } else {
    // TypeScript knows response.message exists
    console.error(response.message);
  }
}

// Multiple unions
type ID = string | number;
type Value = string | number | boolean | null;

// Union in arrays
type MixedArray = (string | number)[];

const arr: MixedArray = ["hello", 42, "world", 100];

Union types allow values to be one of several types. Use | to combine types. TypeScript narrows unions based on type guards. Essential for flexible, type-safe code.

Intersection Types

Intersection types combine multiple types into one. A value must satisfy all types in the intersection. Created using the & operator. What are Intersection Types: • Combines multiple types • Value must satisfy ALL types • Created with & operator • Useful for mixing types • Similar to extending interfaces Use Cases: • Combining object types • Mixing interfaces • Adding properties to existing types • Creating complex types from simple ones • Extending types without modification Intersection vs Union: • Intersection (&): Must satisfy ALL types • Union (|): Must satisfy ONE type • Use intersection to combine • Use union for alternatives

Code Example:
// Basic intersection type
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: string;
  department: string;
};

type EmployeePerson = Person & Employee;

const employee: EmployeePerson = {
  name: "Alice",
  age: 30,
  employeeId: "EMP-001",
  department: "Engineering"
  // Must have properties from both Person and Employee
};

// Intersection with multiple types
type Flyable = {
  fly: () => void;
};

type Swimmable = {
  swim: () => void;
};

type Duck = Person & Flyable & Swimmable;

const duck: Duck = {
  name: "Donald",
  age: 2,
  fly: () => console.log("Flying"),
  swim: () => console.log("Swimming")
};

// Intersection for React component props
type BaseProps = {
  className?: string;
  id?: string;
};

type ButtonProps = {
  onClick: () => void;
  disabled?: boolean;
};

type LinkProps = {
  href: string;
  target?: string;
};

// Component that can be button or link
type FlexibleComponentProps = BaseProps & (ButtonProps | LinkProps);

function FlexibleComponent(props: FlexibleComponentProps) {
  if ("href" in props) {
    // TypeScript knows this is LinkProps
    return <a href={props.href} target={props.target}>Link</a>;
  } else {
    // TypeScript knows this is ButtonProps
    return <button onClick={props.onClick} disabled={props.disabled}>Button</button>;
  }
}

// Intersection with primitives (results in never)
type Impossible = string & number; // never - nothing can be both string and number

// Intersection for extending types
type ReadonlyUser = {
  readonly id: string;
  readonly name: string;
};

type MutableUser = {
  id: string;
  name: string;
  email: string;
};

// Combine readonly base with mutable extension
type ExtendedUser = ReadonlyUser & {
  email: string;
  age: number;
};

// Intersection with function types
type Loggable = {
  log: (message: string) => void;
};

type Cacheable = {
  cache: Map<string, unknown>;
};

type LoggerWithCache = Loggable & Cacheable;

const logger: LoggerWithCache = {
  log: (message) => console.log(message),
  cache: new Map()
};

Intersection types combine multiple types using &. A value must satisfy all types in the intersection. Use intersections to combine object types, mix interfaces, and create complex types from simpler ones.

Literal Types and Template Literal Types

Literal types are types that represent exact values. Template literal types combine string literals, enabling powerful string manipulation at the type level. Literal Types: • Exact value as type • String, number, or boolean literals • Very specific types • Used in unions for enums-like behavior Template Literal Types: • Combine string literal types • Use template literal syntax • Enable string pattern matching • Powerful for API routes, CSS classes, etc. Use Cases: • Status values • Theme values • API endpoint types • CSS class combinations • Event name patterns

Code Example:
// Literal types
type Status = "pending" | "success" | "error";
type Answer = "yes" | "no";
type Number = 42; // Literal number type

const status: Status = "pending"; // ✅ OK
const status2: Status = "invalid"; // ❌ Error

// Literal types in functions
function setTheme(theme: "light" | "dark") {
  console.log(`Theme set to ${theme}`);
}

setTheme("light"); // ✅ OK
setTheme("blue"); // ❌ Error

// Template literal types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = "/users" | "/posts" | "/comments";

type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;

// Results in: "GET /users" | "GET /posts" | "GET /comments" | "POST /users" | ...

const endpoint: ApiEndpoint = "GET /users"; // ✅ OK
const endpoint2: ApiEndpoint = "PATCH /users"; // ❌ Error

// Template literals with variables
type EventName = "click" | "hover" | "focus";
type ElementType = "button" | "input" | "div";

type EventHandlerName = `on${Capitalize<EventName>}`;

// Results in: "onClick" | "onHover" | "onFocus"

// CSS class combinations
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";

type ButtonClass = `btn-${Color}-${Size}`;

// Results in: "btn-red-sm" | "btn-red-md" | "btn-red-lg" | "btn-blue-sm" | ...

// API route patterns
type Resource = "user" | "post" | "comment";
type Action = "create" | "read" | "update" | "delete";

type ApiRoute = `/api/${Resource}/${Action}`;

// Results in: "/api/user/create" | "/api/user/read" | "/api/post/create" | ...

// Template literal with unions
type Lang = "en" | "fr" | "es";
type Page = "home" | "about" | "contact";

type LocalizedRoute = `/${Lang}/${Page}`;

// Results in: "/en/home" | "/en/about" | "/fr/home" | ...

// Advanced template literal patterns
type EmailDomain = "gmail.com" | "yahoo.com" | "example.com";
type Email = `${string}@${EmailDomain}`;

const email: Email = "user@gmail.com"; // ✅ OK
const email2: Email = "user@invalid.com"; // ❌ Error

// React component with literal types
type ButtonVariant = "primary" | "secondary" | "danger";
type ButtonSize = "small" | "medium" | "large";

type ButtonProps = {
  variant: ButtonVariant;
  size: ButtonSize;
  className?: string;
};

function Button({ variant, size, className }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size} ${className || ""}`}
    >
      Click me
    </button>
  );
}

Literal types represent exact values. Template literal types combine string literals using template syntax. Use them for status values, API routes, CSS classes, and any string pattern matching needs.

Mapped Types

Mapped types create new types by transforming properties of existing types. They're powerful for creating variations of types. What are Mapped Types: • Transform properties of existing types • Create new types from old ones • Use keyof to iterate over properties • Enable type transformations • Built-in utility types use mapped types Common Patterns: • Making all properties optional • Making all properties readonly • Making all properties required • Transforming property types • Filtering properties Built-in Mapped Types: • Partial<T> - All properties optional • Required<T> - All properties required • Readonly<T> - All properties readonly • Pick<T, K> - Select properties • Omit<T, K> - Exclude properties

Code Example:
// Basic mapped type
type Optional<T> = {
  [K in keyof T]?: T[K];
};

type User = {
  id: string;
  name: string;
  email: string;
};

type OptionalUser = Optional<User>;
// Result: { id?: string; name?: string; email?: string; }

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

type ReadonlyUser = Readonly<User>;
// Result: { readonly id: string; readonly name: string; readonly email: string; }

// Transforming property types
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type NullableUser = Nullable<User>;
// Result: { id: string | null; name: string | null; email: string | null; }

// Conditional transformation
type Stringify<T> = {
  [K in keyof T]: string;
};

type StringifiedUser = Stringify<User>;
// Result: { id: string; name: string; email: string; } (all become string)

// Filtering properties
type StringKeys<T> = {
  [K in keyof T as K extends string ? K : never]: T[K];
};

// Pick specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type UserName = Pick<User, "name" | "email">;
// Result: { name: string; email: string; }

// Omit properties
type Omit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

type UserWithoutEmail = Omit<User, "email">;
// Result: { id: string; name: string; }

// React component props transformation
type ComponentProps<T> = {
  [K in keyof T]: T[K] | React.ReactNode;
};

type UserComponentProps = ComponentProps<User>;
// All properties can now be React nodes

// Making nested properties optional
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type NestedUser = {
  id: string;
  profile: {
    name: string;
    age: number;
  };
};

type PartialNestedUser = DeepPartial<NestedUser>;
// Result: { id?: string; profile?: { name?: string; age?: number; } }

Mapped types transform properties of existing types. Use them to make properties optional, readonly, or transform their types. Built-in utility types like Partial, Required, and Readonly are implemented using mapped types.

Conditional Types

Conditional types select types based on conditions. They're TypeScript's way of doing type-level if/else statements. What are Conditional Types: • Types that depend on other types • Syntax: T extends U ? X : Y • Type-level conditionals • Enable powerful type inference • Used in utility types Basic Syntax: • T extends U ? X : Y • If T extends U, result is X • Otherwise, result is Y • Can be nested • Can use infer keyword Use Cases: • Type extraction • Type filtering • Function overloads • Utility type creation • API response typing

Code Example:
// Basic conditional type
type IsString<T> = T extends string ? true : false;

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

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

type StringArrayElement = ArrayElement<string[]>; // string
type NumberArrayElement = ArrayElement<number[]>; // number

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

type FuncReturn = ReturnType<() => string>; // string
type AsyncReturn = ReturnType<() => Promise<number>>; // Promise<number>

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

type FuncParams = Parameters<(a: string, b: number) => void>; // [string, number]

// Non-nullable type
type NonNullable<T> = T extends null | undefined ? never : T;

type CleanString = NonNullable<string | null>; // string
type CleanNumber = NonNullable<number | undefined>; // number

// Flatten array type
type Flatten<T> = T extends (infer U)[] ? U : T;

type Flat = Flatten<string[]>; // string
type NotFlat = Flatten<string>; // string

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

type Resolved = Awaited<Promise<string>>; // string
type NotPromise = Awaited<string>; // string

// React component props extraction
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

function MyComponent({ name }: { name: string }) {
  return <div>{name}</div>;
}

type Props = ComponentProps<typeof MyComponent>; // { name: string }

// Conditional type with union distribution
type ToArray<T> = T extends any ? T[] : never;

type StringOrNumberArray = ToArray<string | number>; // string[] | number[]

// Exclude type
type Exclude<T, U> = T extends U ? never : T;

type WithoutString = Exclude<string | number | boolean, string>; // number | boolean

// Extract type
type Extract<T, U> = T extends U ? T : never;

type OnlyStrings = Extract<string | number | boolean, string>; // string

Conditional types select types based on conditions using T extends U ? X : Y syntax. Use them with infer to extract types, create utility types, and build powerful type systems.

Advanced Type Patterns and Best Practices

Combining type aliases, unions, intersections, and conditional types enables powerful type patterns. Understanding when and how to use these patterns is key to effective TypeScript. Advanced Patterns: • Discriminated unions • Branded types • Recursive types • Type guards • Type assertions • Const assertions Best Practices: • Use type aliases for unions/intersections • Use interfaces for object shapes • Prefer type inference when possible • Use const assertions for literals • Create utility types for reuse • Document complex types • Keep types focused and composable

Code Example:
// Discriminated union (tagged union)
type LoadingState = {
  status: "loading";
};

type SuccessState<T> = {
  status: "success";
  data: T;
};

type ErrorState = {
  status: "error";
  error: string;
};

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

function handleState<T>(state: AsyncState<T>) {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return state.data; // TypeScript knows data exists
    case "error":
      return state.error; // TypeScript knows error exists
  }
}

// Branded types (nominal typing)
type UserID = string & { readonly brand: unique symbol };
type ProductID = string & { readonly brand: unique symbol };

function createUserID(id: string): UserID {
  return id as UserID;
}

function createProductID(id: string): ProductID {
  return id as ProductID;
}

// Prevents mixing up IDs
const userId = createUserID("user-123");
const productId = createProductID("prod-456");

// userId = productId; // ❌ Error: types are incompatible

// Recursive types
type TreeNode<T> = {
  value: T;
  children?: TreeNode<T>[];
};

const tree: TreeNode<string> = {
  value: "root",
  children: [
    { value: "child1" },
    {
      value: "child2",
      children: [{ value: "grandchild" }]
    }
  ]
};

// Type guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(value: unknown) {
  if (isString(value)) {
    // TypeScript knows value is string here
    return value.toUpperCase();
  }
  return String(value);
}

// Const assertions
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
} as const;

// config.apiUrl = "other"; // ❌ Error: cannot assign

// Type for exact values
type Config = typeof config;
// Result: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; }

// Utility type composition
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;

type User = {
  id: string;
  name: string;
  email: string;
};

type UpdateUser = PartialExcept<User, "id">;
// Result: { id: string; name?: string; email?: string; }

// React component with advanced types
type ComponentVariant = "primary" | "secondary";
type ComponentSize = "sm" | "md" | "lg";

type ComponentProps = {
  variant: ComponentVariant;
  size: ComponentSize;
  children: React.ReactNode;
} & (
  | { as: "button"; onClick: () => void }
  | { as: "div"; onClick?: never }
);

function FlexibleComponent(props: ComponentProps) {
  if (props.as === "button") {
    return (
      <button onClick={props.onClick} className={`${props.variant} ${props.size}`}>
        {props.children}
      </button>
    );
  }
  return <div className={`${props.variant} ${props.size}`}>{props.children}</div>;
}

Combine type features for powerful patterns: discriminated unions for type-safe state, branded types for nominal typing, recursive types for nested structures, and type guards for runtime type checking. Follow best practices for maintainable type systems.

Conclusion

Type aliases and advanced type declarations provide powerful tools for creating flexible, expressive type systems. Use type aliases for unions, intersections, and complex types. Leverage union types for alternatives, intersection types for combinations, literal types for exact values, and template literal types for string patterns. Mapped types transform existing types, and conditional types enable type-level logic. Combine these features to build sophisticated type systems that catch errors at compile time and provide excellent developer experience. Remember: use type aliases for flexibility, interfaces for object shapes, and always prefer composition and clarity over complexity.