안동민 개발노트

본문 시작

인덱스 타입

인덱스 시그니처로 동적 키의 값 타입을 제한하고 인덱스 접근 타입으로 객체 속성의 타입을 안전하게 추출합니다.

타입스크립트의 인덱스 타입(Index Types)은 객체 타입에서 특정 속성의 타입을 동적으로 추출하거나, 객체의 속성 이름을 타입으로 다룰 때 사용되는 고급 기능입니다.

이는 특히 매핑된 타입과 함께 사용될 때 강력한 시너지를 발휘하며, 런타임에 속성에 접근하는 자바스크립트의 동작을 타입 시스템에서 안전하게 표현할 수 있도록 돕습니다.

인덱스 타입은 크게 두 가지 주요 개념을 포함합니다.

인덱스 시그니처 (Index Signatures)와 인덱스 접근 타입 (Indexed Access Types)입니다.


인덱스 시그니처

인덱스 시그니처(Index Signatures)는 객체가 가질 수 있는 속성들의 이름과 값의 타입을 미리 정의하지 않고, 속성 이름의 타입과 속성 값의 타입만으로 객체의 형태를 유연하게 정의할 때 사용됩니다.

이는 주로 객체의 속성 이름이 동적으로 결정되거나, 정해진 수의 속성이 아닌 임의의 속성들을 포함할 수 있는 경우에 유용합니다.

기본 문법은 다음과 같습니다.

interface MyObject {
  [key: KeyType]: ValueType;
}
  • key: 속성 이름을 나타내는 임의의 변수명 (관례적으로 key 또는 prop 사용).
  • KeyType: 속성 이름으로 허용될 수 있는 타입. string·number·symbol·템플릿 문자열 패턴 및 이러한 무한 키 영역의 유니온을 사용할 수 있습니다. 특정 리터럴 키 집합에는 매핑된 타입이나 Record를 사용합니다.
  • ValueType: 해당 속성 이름으로 접근했을 때 얻게 될 값의 타입.

예시를 통해 살펴보겠습니다.

// string 키를 가지며, 모든 값은 number 타입인 객체
interface StringNumberMap {
  [key: string]: number;
}

const scores: StringNumberMap = {
  "math": 90,
  "english": 85,
  "science": 92
};

console.log(scores["math"]); // 90
console.log(scores.english); // 85

// scores["korean"] = "bad"; // Error: 'string' 형식은 'number' 형식에 할당할 수 없습니다.

// number 키를 가지며, 모든 값은 boolean 타입인 배열과 유사한 객체
interface BooleanArrayLike {
  [index: number]: boolean;
}

const checks: BooleanArrayLike = {
  0: true,
  1: false,
  5: true // 중간 인덱스 건너뛰기 가능
};

console.log(checks[0]); // true
// console.log(checks["2"]); // 숫자형 문자열 키는 접근 가능하지만 이 객체에는 2번 속성이 없어 undefined입니다.
// 임의의 비숫자 문자열 키까지 number 인덱스 시그니처가 허용하는 것은 아닙니다.
인덱스 시그니처의 주의사항
  • 모든 속성 일치: 인덱스 시그니처를 정의하면, 해당 키 영역에 포함되는 명시적 속성의 값도 인덱스 시그니처의 값 타입과 호환되어야 합니다.

    interface UserProfile {
      name: string; // 명시적 속성
      [key: string]: string; // 인덱스 시그니처 (모든 string 키는 string 값을 가져야 함)
    }
    
    const user: UserProfile = {
      name: "Alice",
      city: "Seoul" // 'city'는 인덱스 시그니처에 의해 허용됨
    };
    
    // interface InvalidProfile {
    //   id: number; // Error: Numeric index signature is missing. or string is not assignable to number
    //   [key: string]: string;
    // }
    // 위와 같이 id: number는 string: string과 충돌하므로 오류 발생
    // 만약 허용하려면 [key: string]: string | number; 와 같이 유니온으로 확장해야 합니다.
  • 숫자 인덱스 시그니처: [index: number]: ValueType 형태로 정의할 수 있으며, 이는 배열과 유사한 객체에 사용됩니다. 숫자와 문자열 인덱스 시그니처를 함께 정의하면 숫자 인덱스의 값 타입이 문자열 인덱스의 값 타입에 할당 가능해야 합니다. 이는 자바스크립트에서 숫자로 된 속성 이름이 내부적으로 문자열로 변환되기 때문입니다.

    interface NumberAndStringIndex {
      [key: number]: string;
      [key: string]: string; // string 인덱스 시그니처가 숫자 인덱스 시그니처를 포괄해야 함
    }

인덱스 시그니처는 편리하지만, 한 번 열어 둔 키 범위가 명시 속성과 숫자 키 규칙까지 함께 제약한다는 점을 꼭 기억해야 합니다.

인덱스 시그니처와 런타임 존재 경계

OPEN KEY DOMAIN · POSSIBLE ABSENCE

인덱스 시그니처와 런타임 존재 경계

열린 key/value 정적 계약은 lookup 결과의 규칙을 말할 뿐 실제 key가 존재한다고 보장하지 않는다.

인덱스 시그니처와 런타임 존재 경계 string/number/symbol/pattern key domain에서 명시 속성 호환과 numeric coercion을 거쳐 lookup type이 정해지지만, 실제 key 부재는 런타임 undefined이며 noUncheckedIndexedAccess/in 검사가 그 간극을 드러내는 흐름을 보여 준다. KEY DOMAINstring · number· symboltemplate pattern 포함COMPAT명시 속성 호환index value 규칙 만족COERCIONnumeric key2와 '2'는 같은 속성LOOKUP정적 결과 V기본 설정의 낙관RUNTIME없는 key →undefinedin · hasOwn으로 확인OPTIONnoUncheckedIndexedAccessV | undefined 노출
  1. string · number · symbol

    KEY DOMAIN — template pattern 포함

  2. 명시 속성 호환

    COMPAT — index value 규칙 만족

  3. numeric key

    COERCION — 2와 '2'는 같은 속성

  4. 정적 결과 V

    LOOKUP — 기본 설정의 낙관

  5. 없는 key → undefined

    RUNTIME — in · hasOwn으로 확인

  6. noUncheckedIndexedAccess

    OPTION — V | undefined 노출

number와 string signature를 함께 쓰면 number 값 타입이 string 값 타입에 할당 가능해야 한다.


인덱스 접근 타입

키 도메인과 값 타입 투영

OPEN · FINITE · EXISTING SHAPE

키 도메인과 값 타입 투영

키 집합의 성격에 따라 열린 index signature, 유한 Record/mapped type, 기존 T의 keyof/T[K] projection을 구분한다.

키 도메인과 값 타입 투영 unknown/open key면 index signature, finite key union이면 Record/mapped type을 선택하고, 기존 T에서는 keyof T가 key domain을 만들고 T[K]가 해당 value type을 projection하는 관계를 분리한다. QUESTIONkey domain은?열림 · 유한 · 기존 구조OPENindex signature임의 key의 동일 value 규칙FINITERecord<K,V>literal union 전체 요구EXISTINGkeyof T실제 key domain 계산PROJECTT[K]선택 key의 value unionCHECK런타임 존재정적 projection과 별도
  1. key domain은?

    QUESTION — 열림 · 유한 · 기존 구조

  2. index signature

    OPEN — 임의 key의 동일 value 규칙

  3. Record<K,V>

    FINITE — literal union 전체 요구

  4. keyof T

    EXISTING — 실제 key domain 계산

  5. T[K]

    PROJECT — 선택 key의 value union

  6. 런타임 존재

    CHECK — 정적 projection과 별도

Record<string,V>는 닫힌 레코드가 아니다. 유한 K일 때만 누락 key를 정적으로 요구한다.

인덱스 접근 타입(Indexed Access Types / Lookup Types)은 기존 객체 타입에서 특정 속성의 타입을 T[K] 문법을 사용하여 추출하는 기능입니다.

마치 자바스크립트에서 obj[key]로 속성 값에 접근하듯이, 타입스크립트에서는 Type[Key]로 속성의 타입에 접근합니다.

기본 문법은 다음과 같습니다.

type PropertyType = SomeObjectType[KeyType];
  • SomeObjectType: 속성 타입을 추출할 대상 객체 타입 (인터페이스, 타입 별칭, 클래스 등).
  • KeyType: 추출하고자 하는 속성 이름의 타입. 이는 string 리터럴, number 리터럴, symbol 리터럴, 또는 keyof SomeObjectType으로 얻어진 유니온 타입 등 속성 이름에 해당하는 타입이어야 합니다.

예시를 통해 살펴보겠습니다.

interface UserData {
  id: number;
  name: string;
  email: string;
  address: {
    street: string;
    city: string;
    zipCode: string;
  };
}

// 1. 특정 속성의 타입 추출
type UserId = UserData['id'];     // type UserId = number
type UserName = UserData['name']; // type UserName = string

// 2. 중첩된 객체의 속성 타입 추출
type UserAddress = UserData['address'];       // type UserAddress = { street: string; city: string; zipCode: string; }
type UserCity = UserData['address']['city']; // type UserCity = string

// 3. 유니온 타입을 사용하여 여러 속성들의 타입 유니온 추출
type UserContactInfo = UserData['email' | 'name']; // type UserContactInfo = string

// 4. 'keyof'와 함께 사용하기
// AllUserPropertyTypes는 UserData의 모든 속성 타입들의 유니온입니다.
type AllUserPropertyTypes = UserData[keyof UserData];
// type AllUserPropertyTypes = string | number | { street: string; city: string; zipCode: string; }

// 이 타입은 특정 속성 값을 가질 수 있음을 나타냅니다.
let value1: AllUserPropertyTypes = 123;
let value2: AllUserPropertyTypes = "some text";
let value3: AllUserPropertyTypes = { street: "Main St", city: "LA", zipCode: "90210" };
// let value4: AllUserPropertyTypes = true; // Error: 'boolean' 형식은 'AllUserPropertyTypes' 형식에 할당할 수 없습니다.

keyof 연산자와 인덱스 접근 타입을 결합하면 객체의 모든 속성 타입을 유니온으로 만들거나, 특정 타입 유틸리티를 구현할 때 매우 유용합니다.


인덱스 타입의 활용

인덱스 타입은 매핑된 타입과 함께 사용하여 복잡한 타입 변환 로직을 구현하는 데 핵심적인 역할을 합니다.

동적으로 속성 타입을 추출하는 함수 만들기
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const myUser = { id: 1, name: "Charlie", age: 40 };
const userName = getProperty(myUser, "name"); // userName의 타입은 string
const userId = getProperty(myUser, "id");     // userId의 타입은 number

console.log(userName); // Charlie
console.log(userId);   // 1

// getProperty(myUser, "address"); // Error: Argument of type '"address"' is not assignable to parameter of type '"id" | "name" | "age"'.

이 함수에서 T[K]는 obj[key]가 반환하는 값의 타입을 정확하게 추론할 수 있게 해줍니다.

타입 안전한 Record/Dictionary 구현
type AppConfig = Record<string, string | number | boolean>;

const config: AppConfig = {
  theme: "dark",
  fontSize: 16,
  enableLogging: true,
  "api-key": "xyz123"
};

console.log(config.theme);       // dark
console.log(config["fontSize"]); // 16
// console.log(config.nonExistent); // 열린 string 키이므로 이 속성만 없다는 이유로 거부하지 않습니다. 실제 값은 undefined입니다.
                                  // noUncheckedIndexedAccess는 결과 타입에 undefined를 더할 수 있습니다.
                                  // noPropertyAccessFromIndexSignature는 점 표기 대신 대괄호 표기를 요구할 수 있습니다.

Record<K, T> 유틸리티 타입 자체가 [P in K]: T 형태의 매핑된 타입으로 구현되어 있으며, 인덱스 시그니처의 개념을 포함합니다.

동적 객체 타입은 허용할 키 범위와 반환할 값 타입을 먼저 정해야 안전합니다.


인덱스 타입은 타입스크립트가 자바스크립트의 유연한 객체 구조를 정적 타입 시스템으로 안전하게 다루도록 돕는 강력한 도구입니다.

인덱스 시그니처는 동적인 객체 형태를 정의할 때, 인덱스 접근 타입은 기존 객체 타입에서 특정 속성 타입을 추출할 때 사용됩니다.

이 둘을 함께 이해하고 적절히 활용하면 복잡한 데이터 구조와 상호작용하는 코드를 더 타입 안전하고 견고하게 만들 수 있습니다.