객체 타입 — 선택적 속성, readonly, Record, Index Signature
TypeScript의 객체 타입은 속성의 이름과 타입 을 정의하는 것이며, 선택적 속성, 읽기 전용, 인덱스 시그니처 등으로 세밀하게 제어할 수 있습니다.
기본 객체 타입
// 객체 타입 정의
type User = {
name: string;
age: number;
email: string;
};
const user: User = {
name: '홍길동',
age: 25,
email: 'hong@example.com',
};
// ❌ 정의에 없는 속성은 추가할 수 없음 (초과 속성 검사)
// const wrong: User = { name: '홍길동', age: 25, email: 'a@b.c', phone: '010' };
선택적 속성(Optional Properties)
type Config = {
host: string;
port: number;
ssl?: boolean; // 선택적 — 있어도 되고 없어도 됨
timeout?: number; // 선택적
};
// ssl과 timeout 없이도 OK
const config: Config = {
host: 'localhost',
port: 3000,
};
선택적 속성의 타입은 T | undefined와 동일합니다.
function getTimeout(config: Config): number {
// config.timeout은 number | undefined
return config.timeout ?? 5000; // undefined일 때 기본값
}
exactOptionalPropertyTypes
tsconfig.json에서 exactOptionalPropertyTypes를 켜면, 선택적 속성에 undefined를 직접 할당하는 것을 금지합니다.
// exactOptionalPropertyTypes: true 일 때
const config: Config = {
host: 'localhost',
port: 3000,
// ssl: undefined, // ❌ Error — 아예 안 쓰거나 boolean 값을 줘야 함
};
readonly 속성
type Point = {
readonly x: number;
readonly y: number;
};
const point: Point = { x: 10, y: 20 };
// point.x = 30; // ❌ Error: Cannot assign to 'x' because it is a read-only property
// 주의: readonly는 얕은(shallow) 제약
type UserProfile = {
readonly name: string;
readonly address: {
city: string;
};
};
const profile: UserProfile = {
name: '홍길동',
address: { city: '서울' },
};
// profile.name = '김철수'; // ❌ Error
profile.address.city = '부산'; // ⚠️ OK — 중첩 객체는 수정 가능
면접에서 "readonly는 깊은(deep) 불변성을 보장하나요?"라고 물어보면, 아니요 라고 답해야 합니다.
Index Signature
속성 이름을 미리 알 수 없지만, 타입 패턴은 알 때 사용합니다.
// 키가 string이고 값이 number인 객체
type NumberMap = {
[key: string]: number;
};
const scores: NumberMap = {
math: 90,
english: 85,
science: 95,
};
scores['history'] = 88; // OK — 어떤 문자열 키든 가능
인덱스 시그니처와 명시적 속성 혼합
type Dictionary = {
[key: string]: string;
length: string; // OK — string 타입이므로 인덱스 시그니처와 호환
// length: number; // ❌ Error — number는 string과 호환 안 됨
};
인덱스 시그니처가 있으면 모든 명시적 속성도 그 타입과 호환되어야 합니다.
// 해결: 유니온으로
type FlexibleDict = {
[key: string]: string | number;
length: number; // OK
name: string; // OK
};
Record 유틸리티 타입
Record<K, V>는 인덱스 시그니처의 간편한 대안입니다.
// Record<키 타입, 값 타입>
type Scores = Record<string, number>;
const scores: Scores = {
math: 90,
english: 85,
};
// 키를 유니온으로 제한할 수도 있음
type Subject = 'math' | 'english' | 'science';
type SubjectScores = Record<Subject, number>;
const subjectScores: SubjectScores = {
math: 90,
english: 85,
science: 95,
// history: 88, // ❌ Error — 'history'는 Subject에 없음
};
Record vs Index Signature
// Index Signature — 어떤 키든 허용
type FlexibleMap = { [key: string]: number };
// Record — 동일한 결과
type FlexibleMap2 = Record<string, number>;
// Record의 장점: 키를 유니온으로 제한 가능
type StrictMap = Record<'a' | 'b' | 'c', number>;
// { a: number; b: number; c: number } — 세 키가 모두 필수
초과 속성 검사(Excess Property Checking)
type User = {
name: string;
age: number;
};
// 객체 리터럴을 직접 할당하면 초과 속성 검사가 동작
// const user: User = { name: '홍길동', age: 25, email: 'test' }; // ❌ Error
// 변수를 통해 할당하면 초과 속성 검사를 피함
const data = { name: '홍길동', age: 25, email: 'test' };
const user: User = data; // ✅ OK — email은 무시됨
공부하다 보니 이 동작이 헷갈릴 수 있는데, TypeScript는 구조적 타이핑을 따르기 때문에 "필요한 속성이 있으면 OK"입니다. 초과 속성 검사는 객체 리터럴에만 적용되는 추가 안전장치입니다.
중첩 객체 타입
type Company = {
name: string;
address: {
city: string;
zipCode: string;
};
employees: {
name: string;
role: string;
}[];
};
// 중첩이 깊어지면 별도 타입으로 분리하는 것이 좋음
type Address = {
city: string;
zipCode: string;
};
type Employee = {
name: string;
role: string;
};
type CompanyClean = {
name: string;
address: Address;
employees: Employee[];
};
정리
- 선택적 속성(
?)의 타입은T | undefined와 같다 readonly는 얕은 불변성만 보장한다- Index Signature는 키 패턴을 정의하고,
Record는 그 간편 버전이다 Record<유니온, T>로 키를 제한하면 모든 키가 필수가 된다- 초과 속성 검사는 객체 리터럴 직접 할당 시에만 동작한다
댓글 로딩 중...