I Tried TypeScript “Named” Arguments for Constructors — Here’s My Take

I’m Kayla. I write code all day. I also break it sometimes. Then I fix it. You know what? One tiny thing saved me a lot of time this year: using “named” arguments for class constructors in TypeScript. It’s not a real language feature. It’s a pattern. But it feels real once you use it.
If you’re curious about how this pattern shows up in everyday JavaScript as well, the wider community often calls it the named-arguments pattern.

And yes, I’ve used it in my own apps. On a late night, tea in hand, cat on keyboard. Let me explain.

The pain: positional args make my brain hurt

I used to write classes like this:

class User {
  id: string;
  name: string;
  email?: string;
  isActive: boolean;

  constructor(id: string, name: string, email?: string, isActive: boolean = true) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.isActive = isActive;
  }
}

// Call site
const u = new User("u1", "Kayla", undefined, false);

Now read that call. What is false here? Is it isActive? Is email missing? I had to count the commas. Not fun.

The switch: one object, clear names

So I changed to “named” args. It’s just one object. But it reads like a story. If you’d like an expanded, real-world walkthrough, I’ve published one on ImprovingCode.com that pairs perfectly with the examples below.
For readers who want every gritty detail, you can dig into the full deep-dive here: I tried TypeScript “named” arguments for constructors — here’s my take.
For a succinct overview from a different angle, there’s also this Medium explainer on named arguments for constructors in TypeScript.

interface UserArgs {
  id: string;
  name: string;
  email?: string;
  isActive?: boolean;
}

class User {
  id: string;
  name: string;
  email?: string;
  isActive: boolean;

  constructor({ id, name, email, isActive = true }: UserArgs) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.isActive = isActive;
  }
}

// Call site
const u = new User({
  id: "u1",
  name: "Kayla",
  isActive: false, // very clear
});

I can pass fields in any order. I can see what each value means. My future self says thanks.

Defaults that just work

I like sane defaults. I set them right in the destructuring.

interface ReportArgs {
  title: string;
  author?: string;
  date?: Date;
  pageSize?: "A4" | "Letter";
}

class Report {
  title: string;
  author: string;
  date: Date;
  pageSize: "A4" | "Letter";

  constructor({
    title,
    author = "System",
    date = new Date(),
    pageSize = "A4",
  }: ReportArgs) {
    this.title = title;
    this.author = author;
    this.date = date;
    this.pageSize = pageSize;
  }
}

const r = new Report({ title: "Q3 Numbers" }); // uses all defaults

One nice thing: the defaults kick in when a field is missing or undefined. But not when it’s null. That bit tripped me once.

Real use: a Mailer config that didn’t bite me later

I shipped a small Mailer. Then my PM asked for TLS and timeouts. I didn’t break calls, because the args were named.

interface MailerArgs {
  host: string;
  port?: number;
  secure?: boolean;
  username?: string;
  password?: string;
  timeoutMs?: number;
}

class Mailer {
  host: string;
  port: number;
  secure: boolean;
  username?: string;
  password?: string;
  timeoutMs: number;

  constructor({
    host,
    port = 587,
    secure = false,
    username,
    password,
    timeoutMs = 5000,
  }: MailerArgs) {
    this.host = host;
    this.port = port;
    this.secure = secure;
    this.username = username;
    this.password = password;
    this.timeoutMs = timeoutMs;
  }
}

const mailer = new Mailer({
  host: "smtp.myapp.com",
  username: "no-reply",
  password: "secret",
  secure: true,
});

Later, I added timeoutMs. Old calls still worked. New calls could set the field. No headache. No reorder mess.

Tiny add-on: type once, reuse everywhere

I like to reuse the args type for factories and helpers.

interface ProductArgs {
  id: string;
  name: string;
  priceCents?: number;
  tags?: string[];
}

class Product {
  id: string;
  name: string;
  priceCents: number;
  tags: string[];

  constructor({
    id,
    name,
    priceCents = 0,
    tags = [],
  }: ProductArgs) {
    this.id = id;
    this.name = name;
    this.priceCents = priceCents;
    this.tags = tags;
  }

  static freeSample(args: Omit<ProductArgs, "priceCents">) {
    return new Product({ ...args, priceCents: 0 });
  }
}

const p = Product.freeSample({ id: "p1", name: "Sticker" });

Clear types. Clear calls. Less guessing.

A few “gotchas” I hit (and how I handled them)

  • Extra fields: Passing an object literal with unknown fields will warn. I like that. If I truly need extra stuff, I capture it.

    interface WithRest extends UserArgs {
      [key: string]: unknown;
    }
    
    class UserWithRest extends User {
      rest: Record<string, unknown>;
      constructor(args: WithRest) {
        const { id, name, email, isActive, ...rest } = args;
        super({ id, name, email, isActive });
        this.rest = rest;
      }
    }
    
  • Destructuring and “parameter properties”: You can’t do this neat trick:

    // Not allowed:
    // constructor(public { id, name }: UserArgs) {}
    

    So I keep fields normal and assign inside the body. Simple wins.

  • Null vs undefined: Defaults don’t run on null. If someone sets something: null, it stays null. I check it when needed.

  • Mixing styles: For legacy code, I sometimes support both. But I don’t love it. It adds noise.

    class LegacyUser {
      id: string;
      name: string;
    
      constructor(id: string, name: string);
      constructor(args: { id: string; name: string });
      constructor(a: string | { id: string; name: string }, b?: string) {
        if (typeof a === "string") {
          this.id = a;
          this.name = b!;
        } else {
          this.id = a.id;
          this.name = a.name;
        }
      }
    }
    

When I don’t use it

  • For two tiny fields with no defaults? I may keep them positional.
  • For perf hot paths in tight loops? I stick to simple calls. Though, honestly, it’s rarely the bottleneck.

One niche scenario where the pattern also shined was while prototyping a personal “message generator” for different chat platforms. The function accepted optional fields like caption, mediaUrl, and isNSFW, and I really didn’t want to confuse them. If you need real-world inspiration (or sample data) for risqué chat content, this curated collection of WhatsApp sexts showcases how people actually phrase, punctuate, and emoji-up their messages, which is handy when you’re seeding test databases or refining content-moderation rules. For a location-specific angle on adult-themed listings, I even browsed the structure of posts over at AdultLook Poway — the page gives a clear look at the fields real advertisers use (think location, rates, and bio snippets), so you can model realistic payloads or sanity-check your own naming conventions before they hit production.

A quick checklist I follow

  • Define Args interface.
  • Make only real requirements required.
  • Put defaults in the destructuring.
  • Keep calls readable at a glance.
  • Don’t hide type errors with broad casts.

Final word: small change, calmer code

This pattern made my code easier to read. It made refactors less scary. The calls tell a story, field by field. It’s not magic. It’s just one object. But it feels nice. And when you’re tired and fixing bugs at 11 pm, nice matters.

If your