TypeScript 인덱스 시그니처(Index Signature) 완벽 가이드

인덱스 시그니처란?

인덱스 시그니처는 객체의 속성 이름을 미리 정의하지 않고, 동적으로 속성에 접근할 수 있도록 하는 TypeScript 문법입니다.

interface StringDictionary {
  [key: string]: string;
}

기본 문법

{
  [key: KeyType]: ValueType;
}

  • key: 변수명 (관례적으로 key, index, prop 등을 사용)
  • KeyType: string, number, symbol 중 하나
  • ValueType: 해당 키로 접근했을 때 반환될 값의 타입

실제 사용 예시

1. 기본 사용법

interface UserDatabase {
  [userId: string]: {
    name: string;
    age: number;
  };
}

const users: UserDatabase = {
  "user001": { name: "김철수", age: 25 },
  "user002": { name: "이영희", age: 30 },
  "user003": { name: "박민수", age: 28 }
};

console.log(users["user001"].name); // "김철수"

2. API 응답 처리

interface ApiResponse {
  [endpoint: string]: unknown;
}

const response: ApiResponse = {
  "/users": [{ id: 1, name: "John" }],
  "/posts": [{ id: 1, title: "Hello" }],
  "/comments": []
};

3. 환경 변수 타입 정의

interface EnvironmentVariables {
  [key: string]: string | undefined;
}

const env: EnvironmentVariables = {
  NODE_ENV: "production",
  API_KEY: "abc123",
  PORT: "3000"
};

키 타입별 특징

string 키

interface StringIndex {
  [key: string]: number;
}

const scores: StringIndex = {
  math: 95,
  english: 88,
  science: 92
};

number 키

interface NumberIndex {
  [index: number]: string;
}

const colors: NumberIndex = {
  0: "red",
  1: "green",
  2: "blue"
};

// 배열과 비슷하게 동작
console.log(colors[0]); // "red"

symbol 키 (TypeScript 4.4+)

interface SymbolIndex {
  [key: symbol]: string;
}

const sym1 = Symbol("id");
const obj: SymbolIndex = {
  [sym1]: "unique-value"
};

값 타입 지정

any vs unknown

// 권장하지 않음 - 타입 안정성 없음
interface UnsafeObject {
  [key: string]: any;
}

// 권장 - 타입 가드 필요
interface SafeObject {
  [key: string]: unknown;
}

const obj: SafeObject = { data: 123 };

// 타입 가드 사용
if (typeof obj.data === "number") {
  console.log(obj.data + 1); // OK
}

Union 타입 사용

interface MixedData {
  [key: string]: string | number | boolean;
}

const config: MixedData = {
  host: "localhost",
  port: 3000,
  ssl: true
};

고정 속성과 함께 사용

interface UserConfig {
  // 고정 속성
  username: string;
  email: string;
  
  // 인덱스 시그니처
  [key: string]: string | number | boolean;
}

const config: UserConfig = {
  username: "john_doe",
  email: "john@example.com",
  theme: "dark",
  notifications: true,
  maxRetries: 3
};

주의사항: 인덱스 시그니처의 값 타입은 고정 속성의 타입을 포함해야 합니다.

// ❌ 에러 발생
interface InvalidConfig {
  username: string;
  [key: string]: number; // username은 string인데 number만 허용
}

// ✅ 올바른 예시
interface ValidConfig {
  username: string;
  [key: string]: string | number;
}

실전 활용 패턴

1. 다국어 지원

interface Translations {
  [key: string]: {
    [language: string]: string;
  };
}

const i18n: Translations = {
  greeting: {
    ko: "안녕하세요",
    en: "Hello",
    ja: "こんにちは"
  },
  farewell: {
    ko: "안녕히 가세요",
    en: "Goodbye",
    ja: "さようなら"
  }
};

2. 폼 데이터 처리

interface FormData {
  [fieldName: string]: string | number | File;
}

function handleFormSubmit(data: FormData) {
  Object.keys(data).forEach(key => {
    console.log(`${key}: ${data[key]}`);
  });
}

3. 캐시 구현

interface Cache<T> {
  [key: string]: {
    value: T;
    timestamp: number;
  };
}

const imageCache: Cache<string> = {
  "profile.jpg": {
    value: "base64encodedstring...",
    timestamp: Date.now()
  }
};

제네릭과 함께 사용

interface Dictionary<T> {
  [key: string]: T;
}

const stringDict: Dictionary<string> = {
  name: "John",
  city: "Seoul"
};

const numberDict: Dictionary<number> = {
  age: 30,
  score: 95
};

const userDict: Dictionary<{ name: string; age: number }> = {
  user1: { name: "Alice", age: 25 },
  user2: { name: "Bob", age: 30 }
};

Record 유틸리티 타입 사용

TypeScript는 인덱스 시그니처를 간단하게 표현할 수 있는 Record 유틸리티 타입을 제공합니다.

// 인덱스 시그니처 방식
interface Dictionary1 {
  [key: string]: number;
}

// Record 유틸리티 타입 (동일한 효과)
type Dictionary2 = Record<string, number>;

// 사용 예시
const scores: Record<string, number> = {
  math: 95,
  english: 88
};

주의사항 및 제한사항

1. 키 타입 제한

// ❌ 에러: 키 타입은 string, number, symbol만 가능
interface Invalid {
  [key: boolean]: string; // Error!
}

2. 선택적 속성과의 충돌

interface Problem {
  name?: string;
  [key: string]: string; // Error: name은 string | undefined
}

// 해결 방법
interface Solution {
  name?: string;
  [key: string]: string | undefined;
}

3. 타입 안정성 저하

interface Loose {
  [key: string]: any;
}

const obj: Loose = { name: "John" };
obj.nonExistent.method(); // 런타임 에러! 컴파일 시 감지 안됨

대안: Mapped Types

더 엄격한 타입 안정성이 필요하다면 Mapped Types를 고려하세요.

type AllowedKeys = "name" | "age" | "email";

type StrictObject = {
  [K in AllowedKeys]: string;
};

const user: StrictObject = {
  name: "John",
  age: "30",
  email: "john@example.com"
  // invalid: "not allowed" // Error!
};

마무리

인덱스 시그니처는 동적 속성을 다룰 때 매우 유용하지만, 타입 안정성이 약해질 수 있습니다.

사용 시 권장사항:

  • 가능하면 unknown을 사용하고 타입 가드 활용
  • 속성 이름을 알 수 있다면 명시적 인터페이스 정의 선호
  • Record 유틸리티 타입으로 간결하게 표현
  • 필요한 경우 Mapped Types로 더 엄격한 타입 정의

적재적소에 인덱스 시그니처를 활용하여 유연하면서도 안전한 TypeScript 코드를 작성하세요! 🚀

댓글 남기기