icon안동민 개발노트

인덱스 타입


 인덱스 타입은 타입스크립트에서 객체의 속성에 동적으로 접근하면서도 타입 안전성을 유지할 수 있게 해주는 강력한 기능입니다.

 이를 통해 객체의 구조를 더 유연하게 다룰 수 있습니다.

인덱스 타입의 개념과 기본 사용법

 인덱스 타입을 사용하면 객체의 속성에 동적으로 접근할 수 있습니다.

interface Person {
    name: string;
    age: number;
}
 
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
 
const person: Person = { name: "Alice", age: 30 };
const name = getProperty(person, "name"); // string
const age = getProperty(person, "age");   // number

 여기서 T[K]는 인덱스 접근 타입으로, T 타입의 K 속성의 타입을 나타냅니다.

인덱스 접근 타입

 인덱스 접근 타입([])을 사용하여 객체의 특정 속성 타입을 추출할 수 있습니다.

type PersonName = Person["name"]; // string
type PersonAge = Person["age"];   // number
 
type PersonProps = Person[keyof Person]; // string | number

 이 기능은 복잡한 타입에서 특정 부분을 추출할 때 유용합니다.

keyof 연산자

 keyof 연산자는 객체 타입의 모든 키를 유니온 타입으로 추출합니다.

type PersonKeys = keyof Person; // "name" | "age"
 
function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys.map(key => obj[key]);
}
 
const person = { name: "Alice", age: 30, city: "New York" };
const values = pluck(person, ["name", "age"]); // ["Alice", 30]

인덱스 시그니처

 인덱스 시그니처를 사용하면 객체의 모든 속성에 대해 동일한 타입을 지정할 수 있습니다.

interface StringMap {
    [key: string]: string;
}
 
const map: StringMap = {
    "key1": "value1",
    "key2": "value2"
};
 
function getValue(obj: StringMap, key: string): string {
    return obj[key]; // 항상 string 타입 반환
}

제네릭과 인덱스 타입의 결합

 제네릭과 인덱스 타입을 결합하면 매우 유연한 함수를 설계할 수 있습니다.

function getDeepValue<T, K1 extends keyof T, K2 extends keyof T[K1]>(
    obj: T,
    key1: K1,
    key2: K2
): T[K1][K2] {
    return obj[key1][key2];
}
 
const person = {
    name: { first: "Alice", last: "Johnson" },
    age: 30
};
 
const firstName = getDeepValue(person, "name", "first"); // "Alice"

매핑된 타입과 인덱스 타입의 조합

 매핑된 타입과 인덱스 타입을 함께 사용하면 복잡한 타입 변환을 수행할 수 있습니다.

type Nullable<T> = { [K in keyof T]: T[K] | null };
 
interface User {
    id: number;
    name: string;
}
 
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; }

타입 안전 객체 조작 함수 구현

 인덱스 타입을 활용하여 타입 안전 객체 조작 함수를 구현할 수 있습니다.

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    const result = {} as Pick<T, K>;
    keys.forEach(key => result[key] = obj[key]);
    return result;
}
 
const person = { name: "Alice", age: 30, city: "New York" };
const nameAndAge = pick(person, ["name", "age"]);
// { name: string; age: number; }

인덱스 타입과 조건부 타입의 조합

 인덱스 타입과 조건부 타입을 조합하면 더욱 복잡한 타입 연산이 가능합니다.

type ExtractType<T, U> = {
    [K in keyof T]: T[K] extends U ? K : never
}[keyof T];
 
interface Person {
    name: string;
    age: number;
    isAdmin: boolean;
}
 
type StringProps = ExtractType<Person, string>; // "name"
type NumberProps = ExtractType<Person, number>; // "age"

인덱스 타입 사용 시 타입 추론 문제와 해결 전략

 때때로 타입스크립트는 인덱스 타입을 사용할 때 정확한 타입을 추론하지 못할 수 있습니다. 이런 경우 명시적 타입 주석이나 타입 단언을 사용할 수 있습니다.

function getProp<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
 
const person = { name: "Alice", age: 30 };
const name = getProp(person, "name");
// const name: string | number
 
// 해결 방법:
const name = getProp(person, "name") as string;
// 또는
const name: string = getProp(person, "name");

설계 원칙과 Best Practices

  1. 타입 안전성 유지 : 인덱스 타입을 사용할 때는 항상 타입 안전성을 고려하세요.
  2. keyof 활용 : 객체의 키를 다룰 때는 keyof를 적극 활용하세요.
  3. 제네릭 사용 : 재사용 가능한 유틸리티 함수를 만들 때 제네릭과 인덱스 타입을 결합하세요.
  4. 인덱스 시그니처 주의 : 인덱스 시그니처는 필요한 경우에만 사용하고, 가능한 구체적인 타입을 사용하세요.
  5. 복잡성 관리 : 너무 복잡한 인덱스 타입 연산은 가독성을 해칠 수 있으므로 적절히 분리하세요.
  6. 문서화 : 복잡한 인덱스 타입 사용은 주석이나 문서로 설명하세요.
  7. 타입 추론 활용 : 가능한 한 타입스크립트의 타입 추론을 활용하되, 필요한 경우 명시적 타입을 제공하세요.
  8. 테스트 작성 : 인덱스 타입을 사용한 복잡한 타입 로직은 단위 테스트로 검증하세요.
  9. 실제 사용 사례 기반 : 실제 문제 해결을 위해 인덱스 타입을 사용하고, 과도한 추상화는 피하세요.
  10. IDE 지원 고려 : 인덱스 타입 사용이 IDE의 자동 완성과 타입 추론을 방해하지 않는지 확인하세요.

 타입 매핑과 마찬가지로 인덱스 타입은 타입스크립트에서 객체와 그 속성을 더욱 유연하고 타입 안전하게 다룰 수 있게 해주는 강력한 기능입니다.