TypeScript의 객체 타입은 속성의 이름과 타입 을 정의하는 것이며, 선택적 속성, 읽기 전용, 인덱스 시그니처 등으로 세밀하게 제어할 수 있습니다.

기본 객체 타입

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)

TYPESCRIPT
type Config = {
  host: string;
  port: number;
  ssl?: boolean;       // 선택적 — 있어도 되고 없어도 됨
  timeout?: number;    // 선택적
};

// ssl과 timeout 없이도 OK
const config: Config = {
  host: 'localhost',
  port: 3000,
};

선택적 속성의 타입은 T | undefined와 동일합니다.

TYPESCRIPT
function getTimeout(config: Config): number {
  // config.timeout은 number | undefined
  return config.timeout ?? 5000; // undefined일 때 기본값
}

exactOptionalPropertyTypes

tsconfig.json에서 exactOptionalPropertyTypes를 켜면, 선택적 속성에 undefined를 직접 할당하는 것을 금지합니다.

TYPESCRIPT
// exactOptionalPropertyTypes: true 일 때
const config: Config = {
  host: 'localhost',
  port: 3000,
  // ssl: undefined, // ❌ Error — 아예 안 쓰거나 boolean 값을 줘야 함
};

readonly 속성

TYPESCRIPT
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

속성 이름을 미리 알 수 없지만, 타입 패턴은 알 때 사용합니다.

TYPESCRIPT
// 키가 string이고 값이 number인 객체
type NumberMap = {
  [key: string]: number;
};

const scores: NumberMap = {
  math: 90,
  english: 85,
  science: 95,
};

scores['history'] = 88; // OK — 어떤 문자열 키든 가능

인덱스 시그니처와 명시적 속성 혼합

TYPESCRIPT
type Dictionary = {
  [key: string]: string;
  length: string;    // OK — string 타입이므로 인덱스 시그니처와 호환
  // length: number; // ❌ Error — number는 string과 호환 안 됨
};

인덱스 시그니처가 있으면 모든 명시적 속성도 그 타입과 호환되어야 합니다.

TYPESCRIPT
// 해결: 유니온으로
type FlexibleDict = {
  [key: string]: string | number;
  length: number;  // OK
  name: string;    // OK
};

Record 유틸리티 타입

Record<K, V>는 인덱스 시그니처의 간편한 대안입니다.

TYPESCRIPT
// 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

TYPESCRIPT
// 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)

TYPESCRIPT
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"입니다. 초과 속성 검사는 객체 리터럴에만 적용되는 추가 안전장치입니다.

중첩 객체 타입

TYPESCRIPT
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>로 키를 제한하면 모든 키가 필수가 된다
  • 초과 속성 검사는 객체 리터럴 직접 할당 시에만 동작한다
댓글 로딩 중...