TypeScript Class Constructor Named Parameters: How-to Guide

typescript classes constructors destructuring javascript

TypeScript class constructor named parameters aren't a language feature, but you get the same ergonomics by having your constructor accept a single object and destructuring it. Define an interface (or type) for the parameter shape, then destructure in the constructor signature. This approach gives you call-site clarity, optional fields, and defaults without overloads.

interface UserOptions {
  id: string;
  email: string;
  displayName?: string;
  isAdmin?: boolean;
}

class User {
  id: string;
  email: string;
  displayName: string;
  isAdmin: boolean;

  constructor({ id, email, displayName = "anonymous", isAdmin = false }: UserOptions) {
    this.id = id;
    this.email = email;
    this.displayName = displayName;
    this.isAdmin = isAdmin;
  }
}

const user = new User({ id: "u_1", email: "ada@example.com", isAdmin: true });

Why This Pattern Wins Over Positional Arguments

Positional constructors break down the moment you have more than two or three parameters. Readers at the call site cannot tell what new User("u_1", "ada@example.com", "anonymous", true) means without jumping to the definition, and adding a new optional parameter in the middle of the list is a breaking change. With an options object, every argument is labeled, order doesn't matter, and adding a new optional field is fully backward compatible.

Cutting the Boilerplate With Parameter Properties

Assigning each field twice (once on the class, once in the constructor) is tedious. You can't use TypeScript's public or private parameter properties directly on a destructured argument, but you can combine destructuring with Object.assign and an intersection type. More cleanly, you can declare the fields once and spread.

interface ServerConfig {
  host: string;
  port: number;
  tls?: boolean;
  timeoutMs?: number;
}

class Server implements ServerConfig {
  host!: string;
  port!: number;
  tls: boolean = false;
  timeoutMs: number = 30_000;

  constructor(options: ServerConfig) {
    Object.assign(this, options);
  }
}

The ! definite assignment assertion tells the compiler that host and port will be initialized even though it can't see the assignment inside Object.assign. Use this sparingly, because it disables a real safety check. For small classes, the explicit assignment version is safer and still readable.

Making Required Fields Actually Required

Here's a common gotcha. If you give the entire options object a default of {}, TypeScript will silently let callers omit required fields. Keep the parameter itself required and only default the individual properties during destructuring.

interface QueryOptions {
  sql: string;
  params?: unknown[];
  timeoutMs?: number;
}

class Query {
  constructor(
    private opts: QueryOptions = { sql: "" }
  ) {}
}

class BetterQuery {
  sql: string;
  params: unknown[];
  timeoutMs: number;

  constructor({ sql, params = [], timeoutMs = 5000 }: QueryOptions) {
    this.sql = sql;
    this.params = params;
    this.timeoutMs = timeoutMs;
  }
}

The first class allows new Query() with no arguments, which produces a broken instance. The second forces callers to pass at least { sql: "..." }. If you need some fields optional and others required, split the interface or use a utility type like Partial<T> intersected with a required subset.

interface HttpClientDefaults {
  timeoutMs: number;
  retries: number;
  userAgent: string;
}

interface HttpClientOptions extends Partial<HttpClientDefaults> {
  baseUrl: string;
}

class HttpClient {
  baseUrl: string;
  timeoutMs: number;
  retries: number;
  userAgent: string;

  constructor({ baseUrl, timeoutMs = 10_000, retries = 3, userAgent = "app/1.0" }: HttpClientOptions) {
    this.baseUrl = baseUrl;
    this.timeoutMs = timeoutMs;
    this.retries = retries;
    this.userAgent = userAgent;
  }
}

When to Skip This Pattern

For classes with one or two obvious parameters, an options object is overkill. new Point(3, 4) reads fine, and new Point({ x: 3, y: 4 }) is noise. The sweet spot for named parameters is three or more fields, or any time you have booleans. new Logger(true, false, true) is unreadable no matter how few arguments it has. Also prefer a static factory like User.fromJson(data) when construction involves validation or async work, because constructors cannot be async and cannot return a different type.

Gotchas to Watch For

Destructured defaults only fire when the property is undefined, not when it is null. If you pass { displayName: null } you get null, not "anonymous". Either validate inputs or type the field as string | null to reflect reality. Second, if you mark a field readonly, the Object.assign shortcut still compiles, but TypeScript cannot verify that every readonly field was actually set. Prefer explicit assignment for readonly fields. Finally, avoid spreading user-supplied objects directly into this in code that handles untrusted input, because a malicious payload could set unexpected properties. Pick fields explicitly when the input crosses a trust boundary.

← Back to all articles