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로 보호한다.
체크리스트
Record의 모든 키가 필수인지 선택적인지 명시한다- 값의 타입을 가능한 정확하게 정의한다
- 접근 전에 키의 존재를 확인한다 (타입 가드)
- 동적 키를 할당할 때는 타입 함수를 쓴다
- 변경되면 안 되는 Record는
Readonly로 보호한다
Record는 편리하지만, 타입 정확성을 놓치기 쉽다. 특히 값의 타입이 union인 경우, 각 속성의 구체적 타입 정보가 사라지므로 주의하자.