← 전체 글로 돌아가기

TypeScript

TypeScript Record 타입 쓸 때 자주 하는 실수

Record 타입은 유연하지만, 값의 타입이 부정확하면 런타임 에러로 터진다. 타입 가드와 readonly를 함께 쓰자.

TypeScript의 Record 타입은 키-값 쌍을 표현할 때 편리하지만, 자주 함정에 빠진다.

첫 번째: 선택적 속성을 명시해야 한다

// 문제: 모든 키가 필수인 줄 알고 코드를 쓴다
type Config = Record<'api' | 'db' | 'cache', string>;

const config: Config = {
  api: 'https://api.example.com',
  db: 'postgres://...',
  // cache가 없어도 컴파일 에러는 안 난다! 하지만 런타임에 undefined
};

console.log(config.cache.toUpperCase()); // 런타임 에러!

// 해결: Partial을 쓰거나
type ConfigPartial = Partial<Record<'api' | 'db' | 'cache', string>>;

// 또는 명시적으로
type ConfigExplicit = {
  api: string;
  db: string;
  cache?: string; // 선택적
};

Record의 모든 키가 필수인지 선택적인지 명확히 해야 한다.

두 번째: 값의 타입이 정확하지 않으면 any처럼 동작한다

// 문제: 값의 타입이 넓다
type StatusMap = Record<string, string | number | boolean>;

const status: StatusMap = {
  count: 5,
  isActive: true,
  message: 'ok'
};

// status.count의 타입은 string | number | boolean
// 따라서 다음 코드는 에러가 나야 하는데, any처럼 작동한다
const doubled = status.count * 2; // 타입 체크 실패!

// 해결: 정확한 타입으로
type StatusMapExact = {
  count: number;
  isActive: boolean;
  message: string;
};

값의 타입을 union으로 넓게 정의하면, 각 속성의 정확한 타입 정보가 사라진다.

세 번째: 빈 Record를 다룰 때 주의한다

// 문제: 빈 Record를 초기화
type UserPreferences = Record<string, boolean>;

const prefs: UserPreferences = {}; // 컴파일 OK

// 하지만 값에 접근하면
console.log(prefs.darkMode); // undefined인지 false인지 모름

// 해결: 타입 가드 추가
function getUserPreference(prefs: UserPreferences, key: string): boolean {
  if (key in prefs && typeof prefs[key] === 'boolean') {
    return prefs[key];
  }
  return false; // 기본값
}

접근 전에 항상 키의 존재와 타입을 확인한다.

네 번째: 동적 키를 다룰 때 타입을 보호한다

// 문제: 동적 키로 값을 할당
type ConfigRecord = Record<string, string>;
const config: ConfigRecord = {};

// 런타임에 동적으로 키를 할당
const key = 'apiUrl';
config[key] = 123; // 타입 에러! 하지만 런타임에 할당됨

// 해결: 타입 가드 함수
function setConfig<T extends Record<string, any>>(
  record: T,
  key: keyof T,
  value: T[keyof T]
): void {
  record[key] = value;
}

setConfig(config, 'apiUrl', 'https://...'); // 안전

타입스크립트는 런타임을 보장하지 않으므로, 동적 할당은 타입 함수로 감싼다.

다섯 번째: readonly로 의도를 명시한다

// 문제: 의도치 않게 값을 수정
type FeatureFlags = Record<string, boolean>;
const flags: FeatureFlags = { newUI: true };

flags.newUI = false; // 실수로 수정

// 해결: readonly로 보호
type FeatureFlagsReadonly = Readonly<Record<string, boolean>>;
const flags: FeatureFlagsReadonly = { newUI: true };

flags.newUI = false; // 컴파일 에러!

설정이나 플래그처럼 변경되면 안 되는 Record는 Readonly로 보호한다.

체크리스트

  1. Record의 모든 키가 필수인지 선택적인지 명시한다
  2. 값의 타입을 가능한 정확하게 정의한다
  3. 접근 전에 키의 존재를 확인한다 (타입 가드)
  4. 동적 키를 할당할 때는 타입 함수를 쓴다
  5. 변경되면 안 되는 Record는 Readonly로 보호한다

Record는 편리하지만, 타입 정확성을 놓치기 쉽다. 특히 값의 타입이 union인 경우, 각 속성의 구체적 타입 정보가 사라지므로 주의하자.