How to Set Default Values in TypeScript Interfaces (5 Practical Techniques)

If you’ve ever tried to write this in TypeScript:

interface User {
firstName: string;
lastName: string;
age: number = 30; // ❌
}

…you’ve seen the error and thought, “Wait, how do I give this a default?”

Interfaces only describe shape; they don’t exist at runtime, so they can’t hold default values. But in real projects, we still need sensible defaults for user settings, configuration objects, and component props.

In this tutorial, I’ll walk you through 5 practical ways to handle “default values” with TypeScript interfaces, using clear examples and when‑to‑use guidance for each approach.

Quick recap: why interfaces can’t have defaults

In TypeScript:

  • An interface is a compile‑time contract.
  • It disappears after compilation.
  • Default values are a runtime concern.

That’s why this is not allowed:

interface User {
firstName: string;
lastName: string;
// This is invalid, interfaces cannot contain initializers
age: number = 30;
}

So instead of putting defaults in the interface, we put them where code actually runs: in functions, classes, or helper utilities.

Let’s go through the options.

Method 1: Optional properties + nullish coalescing

This is a very common and simple pattern.

We:

interface User {
firstName: string;
lastName: string;
age?: number; // Optional property
}

const createUser = (user: User): User => {
return {
firstName: user.firstName,
lastName: user.lastName,
age: user.age ?? 30, // Default age to 30 if not provided
};
};

const john = createUser({ firstName: "John", lastName: "Miller" });
console.log(john);
// { firstName: "John", lastName: "Miller", age: 30 }

const emily = createUser({ firstName: "Emily", lastName: "Clark", age: 45 });
console.log(emily);
// { firstName: "Emily", lastName: "Clark", age: 45 }

I executed the above example code and added the screenshot below.

How to Set Default Values in TypeScript Interfaces

A couple of details:

  • age?: number means age may be present or not.
  • user.age ?? 30 uses nullish coalescing, so:
    • If age is undefined or null, it uses 30.
    • If age is 0, it keeps 0 (unlike ||).

When I use this

I use this pattern when:

  • I want a clear factory function that “normalizes” data.
  • The result should always have all properties filled in.
  • Multiple parts of the code should benefit from a consistent default.

For example, in a shared user profile type used across different services, I might centralize the defaulting logic in a function like this.

Common mistake

A frequent mistake is using || instead of ?? in Typescript:

age: user.age || 30; // ❌

This will turn 0 into 30, which is usually not what you want for numeric fields.

Method 2: Default values in function parameters (destructuring)

Sometimes you don’t want a factory; you just want defaults when consuming data.

For that, destructuring with default values is a very clean approach.

interface User {
firstName: string;
lastName: string;
age?: number;
}

const createUser = ({ firstName, lastName, age = 30 }: User): User => {
return {
firstName,
lastName,
age,
};
};

const jane = createUser({ firstName: "Jane", lastName: "Walker" });
console.log(jane);
// { firstName: "Jane", lastName: "Walker", age: 30 }

const mike = createUser({ firstName: "Mike", lastName: "Johnson", age: 50 });
console.log(mike);
// { firstName: "Mike", lastName: "Johnson", age: 50 }

I executed the above example code and added the screenshot below.

How to Set TypeScript Interfaces Default Values

Here:

  • The interface still has age?: number.
  • The function parameter applies the default: age = 30.
  • Inside the function, age is always defined.

When I use this

I reach for this pattern when:

  • I’m writing React props or similar configuration objects.
  • I only need defaults at the use site, not globally.
  • I care about readability more than reusability.

In React, for example, it pairs nicely with props:

interface UserCardProps {
firstName: string;
lastName: string;
age?: number;
}

function UserCard({ firstName, lastName, age = 30 }: UserCardProps) {
// use age safely here
}

Limitation

This does not change the underlying object. If you log the object before passing it to the function, age may still be undefined. The default only applies inside the function body.

Method 3: Classes with default values (when you really need a class)

Sometimes you want more than plain objects:

  • Methods on the object
  • Validation
  • Stronger modeling of behavior

In those cases, a class with default values in the constructor can make sense.

interface User {
firstName: string;
lastName: string;
age?: number;
}

class DefaultUser implements User {
firstName: string;
lastName: string;
age: number;

constructor(firstName: string, lastName: string, age: number = 30) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getDisplayName(): string {
return `${this.firstName} ${this.lastName} (${this.age})`;
}
}

const chris = new DefaultUser("Chris", "Roberts");
console.log(chris);
// DefaultUser { firstName: 'Chris', lastName: 'Roberts', age: 30 }

console.log(chris.getDisplayName());
// Chris Roberts (30)

I executed the above example code and added the screenshot below.

Set Default Values in TypeScript Interfaces

Here, the interface defines the structure, and the class implements it with defaults and behavior.

When I use this

I consider this pattern when:

  • I’m already using classes (e.g., with decorators, ORMs, class‑based frameworks).
  • I want methods on my entities.
  • I need to attach validation or transformation logic.

Things to watch out for

Classes behave differently from plain objects:

  • JSON serialization drops methods:
    • JSON.stringify(chris) only keeps data, not getDisplayName.
  • When sending data over HTTP or between services, I usually convert class instances to plain objects first.

If you don’t need methods or advanced behavior, a class can be overkill. One of the next two patterns is usually simpler.

Method 4: Merging defaults with Object.assign or spread

For configuration objects, I often want:

  • A reusable defaults object
  • The ability to override some values
  • A fully populated result type

This is where merging patterns shine.

Using Object.assign

interface UserSettings {
theme?: "light" | "dark";
language?: string;
notificationsEnabled?: boolean;
}

const DEFAULT_SETTINGS: Required<UserSettings> = {
theme: "light",
language: "en-US",
notificationsEnabled: true,
};

function applySettings(settings: UserSettings): Required<UserSettings> {
return Object.assign({}, DEFAULT_SETTINGS, settings);
}

const customSettings = applySettings({ theme: "dark" });
console.log(customSettings);
// { theme: 'dark', language: 'en-US', notificationsEnabled: true }

Key points:

  • DEFAULT_SETTINGS is Required<UserSettings>, so it must include all properties.
  • applySettings returns Required<UserSettings>, so callers always get a fully populated settings object.

Using spread syntax (often my preferred style)

function applySettingsWithSpread(
settings: UserSettings
): Required<UserSettings> {
return { ...DEFAULT_SETTINGS, ...settings };
}

const alexSettings = applySettingsWithSpread({
language: "es-US",
notificationsEnabled: false,
});
console.log(alexSettings);
// { theme: 'light', language: 'es-US', notificationsEnabled: false }

When I use this

This is my go‑to for:

  • Feature flags
  • UI settings
  • API client options
  • Any “options object” pattern

For example, if I’m building a configuration object for a dashboard in a U.S. company, I might default to en-US and America/New_York, and allow overrides.

Method 5: A reusable withDefaults<T> helper (generic and flexible)

If you find yourself merging defaults over and over, it’s worth extracting a generic helper.

interface ProjectConfig {
title: string;
maxMembers: number;
isPrivate: boolean;
tags: string[];
}

function withDefaults<T>(partial: Partial<T>, defaults: T): T {
return { ...defaults, ...partial };
}

const defaultProjectConfig: ProjectConfig = {
title: "Untitled Project",
maxMembers: 10,
isPrivate: false,
tags: [],
};

const salesProject = withDefaults<ProjectConfig>(
{
title: "Sales Dashboard",
isPrivate: true,
},
defaultProjectConfig
);

console.log(salesProject);
// {
// title: 'Sales Dashboard',
// maxMembers: 10,
// isPrivate: true,
// tags: []
// }

Here:

  • Partial<T> lets the caller provide only the properties they care about.
  • defaults: T is a full default object.
  • The return type is TypeScript, so the result is fully populated.

You can reuse withDefaults across many interfaces.

When I use this

This is the pattern I like most in larger codebases:

  • Utility libraries
  • Shared config modules
  • SPFx and frontend projects where many components need the same defaulting behavior

Once the helper exists, new interfaces automatically gain a clean way to apply defaults.

Common issues when dealing with defaults and interfaces

Here are a few issues I’ve seen repeatedly in real projects:

  • Expecting interfaces to apply defaults by themselves
    Interfaces don’t run. If you don’t put defaults in a function, class, or merge helper, nothing will happen at runtime.
  • Overusing classes just to set default values in Typescript
    If you don’t need methods or instanceof, a simple function plus an object literal is usually easier to test and reason about.
  • Forgetting about 0"", and false with ||
    Using || for defaults can turn legitimate values into the default. Prefer ?? or explicit checks.
  • Mixing “schema” and runtime defaults
    Your TypeScript types describe the schema; your functions and helpers should handle defaults. Keeping that separation makes code more predictable.

Which method should you pick?

Here’s a quick way to choose the right approach based on your situation:

  • You want a simple, central place to normalize data
    → Use Method 1 (optional properties + ??) in a factory function.
  • You’re writing component props or function parameters in TypeScript
    → Use Method 2 (destructuring defaults).
  • You genuinely need behavior on the object (methods, validation)
    → Use Method 3 (class with constructor defaults).
  • You’re dealing with config/options objects that may be partial
    → Use Method 4 (Object.assign or spread with a defaults object).
  • You want a reusable, generic solution for many interfaces
    → Use Method 5 (withDefaults<T> helper).

You don’t have to pick only one forever. In a real project, it’s common to combine them. For example, you might:

  • Use a withDefaults helper for configuration
  • Use destructuring defaults for component props
  • Use a class for a specific domain entity that carries behavior

The important part is to decide where you want defaults applied and keep that consistent across the codebase.

You may also read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.