객체 타입 (Object Types)
TypeScript에서는 객체의 구조(Shape)를 타입으로 정의할 수 있다.
각 속성(property)의 이름과 타입을 명시하면, 객체가 올바른 형태를 갖도록 검사할 수 있다.
// 자동차 객체
let car: {
make: string;
model: string;
year: number;
};
car = {
make: 'Volkswagen',
model: 'Tiguan',
year: 2020,
};- 객체 타입을 통해 필요한 속성과 타입을 명확히 지정할 수 있다. (
make,model,year의 타입이 강제) - 예상치 못한 값이나 속성이 들어가는 것을 방지한다.
💡 object 타입과 객체 리터럴 타입의 차이
// 자동차 객체
let car: object = {
make: 'Volkswagen',
model: 'Tiguan',
year: 2020,
};
car.year; // Error: 'object' 형식에 'year' 속성이 없습니다.object 타입은 “이 값이 객체다”라는 정보만 알려줄 뿐, 객체 안에 어떤 속성이 있는지는 알려주지 않는다.
따라서 car.year처럼 특정 속성에 접근하려고 하면 TypeScript는 오류를 발생시킨다.
let car: {
make: string;
model: string;
year: number;
} = {
make: 'Volkswagen',
model: 'Tiguan',
year: 2020,
};
car.year; // 20201. 기본 객체 타입 정의
객체 타입은 아래와 같이 속성명: 타입 형태로 정의한다.
더 복잡한 경우는 인터페이스(interface) 또는 타입 별칭(type alias)을 사용한다.
{
propertyName: propertyType;
}1-1. 객체 리터럴 타입
한 변수에 한정된 구조를 직접 명시하는 방법이다.
// monitor 객체의 타입 명시
let monitor: {
brand: string;
price: number;
};
// 객체 리터럴 할당
monitor = {
brand: 'LG',
price: 100,
};
// property에 직접 값 할당
monitor.brand = 'LG';
monitor.price = 100;
monitor.displaySize = '22inch'; // 오류: 정의되지 않은 프로퍼티1-2. 인터페이스 (Interface)
재사용 가능한 객체 타입을 정의할 때 사용한다. extends를 활용해 확장도 가능하다.
interface Monitor {
brand: string;
price: number;
}
let monitor: Monitor = {
brand: 'LG',
price: 120,
};1-3. 타입 별칭 (Type Alias)
type 키워드로 별칭을 정의해 재사용한다. 인터페이스와 비슷하지만, 유니온/튜플/프리미티브도 표현할 수 있다.
type UserType = {
id: string;
name: string;
age: number;
};
let user1: UserType = { id: '1', name: 'Bin', age: 30 };
// 오류: age 누락
let user2: UserType = { id: '2', name: 'Park' };
// 오류: 정의되지 않은 속성 포함
let user3: UserType = {
id: '3',
name: 'Binny',
age: 25,
email: 'email@email.com',
};배열로도 활용 가능하다.
let users: UserType[] = [];
users.push(user1);
// 오류: age가 누락된 객체
users.push({
id: '2',
name: 'Park',
});
// 오류: 빈 객체
users.push({});
// 오류: 프로퍼티가 부족한 객체를 넣음
users.push({
id: '10',
});1-4. 타입 추론
객체를 초기화하면 TypeScript가 구조를 자동으로 추론한다.
let monitor = {
brand: 'LG',
price: 120,
};
monitor.price = 150; // 가능
monitor.price = '123'; // 오류: number에 string 할당 불가
monitor.displaySize = '22inch'; // 오류: 존재하지 않는 프로퍼티
// TypeScript는 자동으로 { brand: string; price: number } 형태로 추론한다.1-5. 인덱스 시그니처 (Index Signature)
인덱스 시그니처는 객체의 속성 이름이 미리 정해져 있지 않을 때 사용하는 문법이다.
키의 타입과 값의 타입을 지정해, 객체가 어떤 형태의 속성을 가질 수 있는지 정의한다.
// 과목별 점수를 저장하는 객체
type Scores = {
korean: number;
english: number;
math: number;
};
const scores = {
korean: 90,
english: 85,
math: 95,
};- 위 객체는
korean,english,math처럼 속성 이름이 정해져 있다.
하지만 과목이 더 늘어날 수 있다면 모든 속성을 하나씩 타입으로 작성하기 번거로울 수 있다.
이럴 때 인덱스 시그니처를 사용할 수 있다.
type Scores = {
[subject: string]: number;
};
const scores: Scores = {
korean: 90,
english: 85,
math: 95,
};[subject: string]: number;
- 속성 이름은
string타입이다. - 각 속성의 값은
number타입이어야 한다.
🚨 주의
아래 코드는 값의 타입이 number가 아니기 때문에 오류가 발생한다.
const scores: Scores = {
korean: 90,
english: 'A+', // Error: number 타입이어야 함
};🚨 빈 객체도 허용된다.
인덱스 시그니처는 “반드시 어떤 속성이 있어야 한다”는 뜻이 아니라,
“속성이 있다면 이 규칙을 따라야 한다”는 뜻이기 때문이다.
const emptyScores: Scores = {}; // 가능2. 객체의 Property 타입
2-1. 선택 속성 (Optional)
속성이 필수가 아닐 때 ? (선택적/옵셔널 프로퍼티)를 붙인다.
let user: {
id: string;
name: string;
age?: number; // 선택적 속성
};
user = {
id: '1234',
name: 'Binny',
}; // age 없음 → 가능age는 선택 속성이기 때문에 user 객체에 age 프로퍼티가 없어도 오류가 발생하지 않는다.
2-2. 읽기 전용 속성 (Readonly)
수정하면 안 되는 속성은 readonly를 사용한다.
interface Config {
readonly clientKey: string;
url: string;
}
let config: Config = {
clientKey: 'abc123',
url: 'https://api.com',
};
config.url = 'https://new.com';
config.clientKey = 'xyz789'; // 오류3. 중첩 객체 (Nested Object)
객체 안의 객체도 타입으로 정의할 수 있다.
type Payload = {
timestamp: string;
user: {
readonly id: string;
isActive?: boolean;
emails: string[];
};
};
const payload: Payload = {
timestamp: 'event',
user: {
id: '123',
isActive: true,
emails: ['a@email.com', 'b@email.com'],
},
};- 주로 API 요청/응답 구조를 표현할 때 자주 사용한다.
4. 구조적 타입 시스템
TypeScript는 타입의 이름보다 객체의 구조를 기준으로 타입을 판단한다.
즉, 타입 이름이 달라도 필요한 속성과 타입이 같다면 같은 타입처럼 사용할 수 있다.
반대로 명목적 타입 시스템(Nominal Type System)은 타입의 이름이 같아야 같은 타입으로 판단하는 방식이다.
type User = {
id: number;
name: string;
};
type Member = {
id: number;
name: string;
};
let user: User = {
id: 1,
name: 'Binny',
};
let member: Member = user; // 가능User와Member는 이름은 다르지만 구조가 같기 때문에 서로 할당할 수 있다.
5. 객체 타입의 호환성
객체 타입의 호환성도 객체가 가진 프로퍼티 구조를 기준으로 판단한다.
필요한 프로퍼티를 모두 가지고 있다면, 추가 프로퍼티가 있어도 호환될 수 있다.
type Animal = {
name: string;
color: string;
};
type Dog = {
name: string;
color: string;
breed: string;
};
let animal: Animal = {
name: '기린',
color: 'yellow',
};
let dog: Dog = {
name: '돌돌이',
color: 'brown',
breed: '진도',
};
animal = dog; // 가능 (업캐스팅)
dog = animal; // Error (다운캐스팅)Dog 타입은 Animal 타입이 요구하는 name과 color를 모두 가지고 있다.
따라서 Dog 타입의 값은 Animal 타입에 할당할 수 있다.
반대로 Animal 타입에는 breed 프로퍼티가 없을 수 있다.
그래서 Dog 타입의 변수에는 할당할 수 없다.
Animal타입은Dog타입의 슈퍼타입이다.Dog타입은Animal타입의 서브타입이다.
💡 왜 Animal이 Dog의 슈퍼타입일까?
처음 보면 Dog가 더 많은 프로퍼티를 가지고 있으므로 더 넓은 타입처럼 보일 수 있다.
하지만 TypeScript에서는 필요한 프로퍼티가 적은 타입일수록 더 넓은 타입으로 볼 수 있다.
Animal: name, color를 가진 모든 객체Dog: name, color, breed를 가진 모든 객체
Dog 타입의 객체는 항상 Animal 타입의 조건을 만족한다.
하지만 Animal 타입의 객체가 항상 Dog 타입의 조건을 만족하는 것은 아니다.
따라서 Animal은 Dog의 슈퍼타입이고, Dog는 Animal의 서브타입이다.
type Book = {
name: string;
price: number;
};
type ProgrammingBook = {
name: string;
price: number;
skill: string;
};
let book: Book;
let programmingBook: ProgrammingBook = {
name: '한 입 크기로 잘라먹는 리액트',
price: 33000,
skill: 'reactjs',
};
book = programmingBook; // 가능
programmingBook = book; // ErrorProgrammingBook은Book이 요구하는name과price를 모두 가지고 있다.
따라서ProgrammingBook타입의 값은Book타입에 할당할 수 있다.- 하지만
Book타입에는skill프로퍼티가 없을 수 있으므로,ProgrammingBook타입에는 할당할 수 없다.
6. 초과 프로퍼티 검사
객체 타입은 구조를 기준으로 호환성을 판단한다.
하지만 객체 리터럴을 바로 할당할 때는 초과 프로퍼티 검사(Excess Property Check)가 발생한다.
초과 프로퍼티 검사란, 객체 리터럴을 특정 객체 타입에 바로 할당할 때 타입에 정의되지 않은 프로퍼티가 있는지 검사하는 TypeScript의 기능이다.
6-1. 객체 리터럴을 바로 할당하는 경우
type Book = {
name: string;
price: number;
};
let book: Book = {
name: '한 입 크기로 잘라먹는 리액트',
price: 33000,
skill: 'reactjs', // Error
};- 객체 리터럴을 직접 할당하면 TypeScript는 타입에 없는 프로퍼티가 있는지 검사한다.
- 이때
skill은Book타입에 정의되어 있지 않으므로 오류가 발생한다.
6-2. 변수에 담아 할당하는 경우
객체 리터럴을 바로 할당하지 않고, 먼저 변수에 담은 뒤 할당하면 초과 프로퍼티 검사가 발생하지 않는다.
type Book = {
name: string;
price: number;
};
type ProgrammingBook = {
name: string;
price: number;
skill: string;
};
let programmingBook: ProgrammingBook = {
name: '한 입 크기로 잘라먹는 리액트',
price: 33000,
skill: 'reactjs',
};
let book: Book = programmingBook; // 가능programmingBook은Book타입이 요구하는name과price를 모두 가지고 있다.
따라서Book타입 변수에 할당할 수 있다.
book.name; // 가능
book.price; // 가능
book.skill; // Error- 다만
book변수의 타입은Book이기 때문에,skill프로퍼티에는 접근할 수 없다.
6-3. 함수 인수에서도 발생한다
초과 프로퍼티 검사는 함수에 객체 리터럴을 바로 전달할 때도 발생한다.
function printBook(book: Book) {
console.log(book.name, book.price);
}
printBook({
name: '한 입 크기로 잘라먹는 리액트',
price: 33000,
skill: 'reactjs', // Error
});- 함수 인수로 객체 리터럴을 직접 전달했기 때문에 초과 프로퍼티 검사가 발생한다.
변수에 미리 담아 전달하면 오류가 발생하지 않는다.
let programmingBook: ProgrammingBook = {
name: '한 입 크기로 잘라먹는 리액트',
price: 33000,
skill: 'reactjs',
};
printBook(programmingBook); // 가능programmingBook은Book타입이 요구하는name과price를 가지고 있으므로 전달할 수 있다.
👩🏻💻 요약
- TypeScript는 객체의 이름보다 구조를 기준으로 타입을 판단한다.
- 필요한 프로퍼티를 모두 가지고 있다면 객체 타입끼리 호환될 수 있다.
- 프로퍼티가 더 많은 타입은 더 좁은 타입으로 볼 수 있다.
- 객체 리터럴을 바로 할당하면 초과 프로퍼티 검사가 발생한다.
- 초과 프로퍼티 검사는 타입에 정의되지 않은 프로퍼티가 있는지 확인한다.
- 객체를 변수에 먼저 담아 할당하면 초과 프로퍼티 검사가 발생하지 않고, 구조적 타입 호환성을 기준으로 판단한다.