TypeScript Basics #3: interface and type alias
Last time we covered the basic types and wrote object types inline. But when the same shape needs to be used in many places, writing it inline every time quickly becomes messy. In this post we cover two tools for naming and reusing object and function type shapes — interface and type alias.
The limits of inline types #
Look at this code.
function logUser(user: { id: string; name: string; email: string }): void {
console.log(user.name);
}
function saveUser(user: { id: string; name: string; email: string }): void {
// ...
}
const me: { id: string; name: string; email: string } = {
id: 'u-1', name: '철수', email: 'cheolsu@example.com',
};{ id: string; name: string; email: string } is repeated three times. Adding one more field means editing all three places, and a typo or omission silently breaks type checking.
The fix — name this shape.
type alias #
The type keyword lets you give a type a name.
type User = {
id: string;
name: string;
email: string;
};
function logUser(user: User): void {
console.log(user.name);
}
function saveUser(user: User): void {
// ...
}
const me: User = {
id: 'u-1', name: '철수', email: 'cheolsu@example.com',
};Much cleaner. The single name User now refers to the same shape wherever it’s needed.
type can name any type, not just objects.
type ID = string; // primitives too
type Point = [number, number]; // tuple
type Color = 'red' | 'green' | 'blue'; // union
type Maybe<T> = T | null; // generic (covered in #6)
type Callback = (err: Error | null, value: string) => void; // function
The naming convention is usually PascalCase (User, OrderItem).
interface #
The same object type can also be expressed with the interface keyword.
interface User {
id: string;
name: string;
email: string;
}
function logUser(user: User): void {
console.log(user.name);
}Almost the same result as the type alias above.
Unlike a type alias, interface can only describe object and function shapes — not primitives or unions. In most other respects they behave the same.
interface vs type — which should you use? #
This is the most frequent question. Bottom line first:
They behave the same in nearly every case. There are small differences, but you don’t need to worry about them at first.
Most teams pick one of the following and use it consistently.
- type-first — use
typeeverywhere, including for objects. Reason: consistency. The React/Next.js community leans this way. - interface-first —
interfacefor objects,typefor everything else. Reason: it fits objects well and offers a small extra (declaration merging).
This series goes with the type-first style for two reasons: (1) it’s more expressive and handles every case with a single keyword, and (2) it’s the majority choice in the modern React/TS community. That said, you should still get familiar with interface — you’ll see it often when reading library code.
The small differences #
1. Declaration merging #
When interface is declared multiple times with the same name, the declarations are merged automatically.
interface User {
name: string;
}
interface User {
age: number;
}
const u: User = { name: '철수', age: 30 }; // ✓ merged — both required
type errors if you declare it twice with the same name:
type User = { name: string };
type User = { age: number }; // 🚫 error
Declaration merging is useful for augmenting types from external libraries (like Window or Express.Request). It’s rarely used in everyday code, but it’s a powerful feature when you need it.
2. Extension syntax #
interface uses extends for inheritance:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const d: Dog = { name: '바둑이', breed: '진돗개' };type uses & (intersection) to combine:
type Animal = { name: string };
type Dog = Animal & { breed: string };
const d: Dog = { name: '바둑이', breed: '진돗개' };Almost the same result, with subtle differences. In conflicting-property cases the behavior diverges (interface raises an error, while & reduces the conflict to never, etc.), but you won’t notice the difference in everyday use.
3. Things only type can do #
Unions, tuples, function types, and mapped types can only be expressed with a type alias.
type Status = 'pending' | 'active' | 'deleted'; // union
type Point = [number, number]; // tuple
type Reducer<T> = (state: T, action: any) => T; // function signature as a name
interface only works for object/function shapes, so it can’t express these.
Object types in more detail #
Now let’s look at the various ways to write object types with type or interface.
Optional properties #
type User = {
id: string;
name: string;
age?: number; // present or absent — both OK
email?: string;
};
const a: User = { id: 'u-1', name: '철수' }; // ✓
const b: User = { id: 'u-2', name: '영희', age: 28 }; // ✓
const c: User = { id: 'u-3', name: '민수', age: 35, email: 'm@x.com' };// ✓
readonly #
type User = {
readonly id: string;
name: string;
};
const u: User = { id: 'u-1', name: '철수' };
u.name = '영희'; // ✓
u.id = 'u-2'; // 🚫
Index signatures — dynamic keys #
Use these when you don’t know the key names ahead of time.
type StringMap = {
[key: string]: string;
};
const dict: StringMap = {
apple: '사과',
banana: '바나나',
cherry: '체리',
};
dict['durian'] = '두리안'; // can add
{ [key: string]: T } means “an object with string keys and values of type T.” It’s especially useful when you need to mix known keys with dynamic ones.
type FormData = {
name: string;
email: string;
[extra: string]: string; // any other string key with string value is allowed
};Method signatures #
You can also describe a function as part of an object.
type Counter = {
value: number;
increment(): void;
add(n: number): number;
};
const c: Counter = {
value: 0,
increment() { this.value += 1; },
add(n) { return this.value + n; },
};Writing the function signature as a property means the same thing.
type Counter = {
value: number;
increment: () => void;
add: (n: number) => number;
};The two notations are nearly identical, with a subtle difference under the strictFunctionTypes option. Either works fine in everyday code.
Function type aliases #
You can also name function signatures.
type Comparator = (a: number, b: number) => number;
const ascending: Comparator = (a, b) => a - b;
const descending: Comparator = (a, b) => b - a;
[3, 1, 4, 1, 5].sort(ascending);The same function shape can now be reused under the name Comparator wherever it’s needed. It’s a common pattern in library APIs that take many callbacks.
Combining types — in practice #
Patterns for combining existing types into new ones come up a lot.
Combining two types #
type Person = { name: string; age: number };
type Employee = { company: string; salary: number };
type EmployedPerson = Person & Employee;
const me: EmployedPerson = {
name: '철수',
age: 30,
company: 'Acme',
salary: 50000,
};A & B is “a type with all properties of A and all properties of B.”
Picking only some fields #
type User = { id: string; name: string; email: string; age: number };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }
type UserWithoutAge = Omit<User, 'age'>;
// = { id: string; name: string; email: string }
Pick and Omit are built-in TypeScript utility types. They’re used heavily in real codebases. We cover them in detail in #7.
Try it yourself — a small library system #
A small example combining what we’ve covered.
type Book = {
readonly id: string;
title: string;
author: string;
publishedYear?: number;
tags: string[];
};
type Member = {
readonly id: string;
name: string;
email: string;
};
type Loan = {
bookId: string;
memberId: string;
borrowedAt: Date;
dueDate: Date;
returnedAt?: Date;
};
type LoanWithDetails = Loan & {
book: Book;
member: Member;
};
function isOverdue(loan: Loan): boolean {
if (loan.returnedAt) return false;
return new Date() > loan.dueDate;
}
const book1: Book = {
id: 'b-1',
title: '타입스크립트 입문',
author: '저자',
publishedYear: 2024,
tags: ['프로그래밍', '입문'],
};
const member1: Member = {
id: 'm-1',
name: '철수',
email: 'cheolsu@example.com',
};
const loan1: Loan = {
bookId: book1.id,
memberId: member1.id,
borrowedAt: new Date('2026-04-01'),
dueDate: new Date('2026-04-15'),
};
console.log(isOverdue(loan1)); // based on today's date
The type definitions stack up cleanly, and functions like isOverdue operate safely on top of them. This is the real value of TypeScript — declaring data shapes up front and then building functions and components on that foundation.
Common pitfalls #
1. interface duplicate declarations merging unintentionally #
When the same interface name is declared in two places, they merge automatically. That’s great if intentional, but if a different module accidentally uses the same name you get an unexpected merge. In a large codebase, type has the edge in safety here.
2. Excess property checks on object literals #
type User = { name: string };
const u: User = { name: '철수', age: 30 }; // 🚫 age is not on User
const data = { name: '철수', age: 30 };
const v: User = data; // ✓ assigning via a variable is OK (no excess check)
The excess property check is strict only when assigning an object literal directly. Assigning through a variable is more lenient. It looks odd at first, but it’s intentional.
3. Confusing union with intersection #
A | B is “either A or B” (union, #4),
A & B is “the composition of A and B” (intersection).
For objects this can be confusing — remember that intersection is “an object that has the properties of both.” Union is “the shape of one of the two.”
Wrap-up #
This post covered two tools for naming object and function types.
type— can name any type (object, primitive, union, tuple, function, …)interface— only for object/function shapes. Supports declaration merging.- They’re compatible in nearly every case; pick one and use it consistently per team convention.
- Object type features: optional
?,readonly, and index signatures. - Combine types with
&(intersection); pick subsets withPick/Omit.
So far the types we’ve defined have been a single shape — “this is User,” “this is Book.” But real types are often one of multiple possibilities — “this is User or Guest,” “the status is loading or success or error.” In the next post, “TypeScript Basics #4: Union / Literal / Narrowing,” we look at the powerful tools for those cases — union types, literal types, and narrowing.