본문으로 건너뛰기

프론트엔드

객체 타입 심화: 인덱스 시그니처와 초과 속성 검사

2026. 01. 20. 화요일 오전 8시 26분

자바스크립트에서는 데이터를 묶고 주고받을 때 객체로 만들어서 사용합니다. 타입스크립트에서는 이러한 객체의 형태를 타입으로 표현합니다.

단순한 형태의 객체라면 문제가 되지 않지만, 복잡한 형태의 객체인 경우 타입으로 표현하기도 어려워집니다. 복잡한 형태의 객체를 표현하기 위해 알아야 할 사항들과 방법에 대해 정리했습니다.

객체의 표현

객체 타입은 속성의 이름과 타입을 선언하여 표현합니다. type과 interface 두 가지 키워드를 사용하여 타입을 선언할 수 있습니다.

interface User {
  name: string;
  email: string;
}
 
type Product = {
  id: number;
  title: string;
};

옵셔널 속성과 readonly

?를 추가해서 속성을 옵셔널로 표현할 수 있고, readonly를 추가해서 속성에 대한 재할당을 막을 수 있습니다.

interface Config {
  readonly apiKey: string; // 재할당 불가
  timeout?: number; // 선택적 속성
}

인덱스 시그니처(Index Signature)

간혹, 객체가 가지는 모든 속성의 이름은 모르지만, 해당 속성들의 타입은 아는 경우가 있습니다. 이런 경우, 객체가 특정 타입의 추가 속성을 얼마든지 가질 수 있도록 객체 타입을 표현할 수 있습니다.

인덱서의 타입은 string, number, symbol, 그리고 이 타입들의 유니온 타입으로만 표현할 수 있습니다.

interface ColorInfo {
  hex: string;
  tone: string;
}
 
interface ColorTheme {
  [index: string | number]: ColorInfo;
}
 
const appColorTheme: ColorTheme = {
  warn: {
    hex: '#FF0000',
    tone: '생생한 빨간색',
  },
  primary: {
    hex: '#2127DB',
    tone: '선명한 네이비',
  },
  error: '#110011',
  // Type 'string' is not assignable to type 'ColorInfo'.
};

복수 타입을 지원하는 인덱스 시그니처

복수의 타입을 가지는 인덱스 시그니처는 특이한 규칙이 있습니다. 숫자 인덱서의 값 타입은 반드시 문자열 인덱서 값 타입의 서브타입이어야 합니다.

interface Animal {
  name: string;
}
 
interface Pet {
  name: string;
  owner: string;
}
 
// Pet은 Animal의 서브타입입니다.
interface ValidIndexSignature {
  [x: string]: Animal;
  [x: number]: Pet;
}
 
// 숫자 인덱서 자리에는 Pet, 문자열 인덱서 자리에는 Animal이 들어갑니다.
const pet1: Pet = { name: 'pet1', owner: 'me' };
const pet2: Pet = { name: 'pet2', owner: 'you' };
 
const validIndexSignature: ValidIndexSignature = {
  animal1: { name: 'animal1' },
  1: pet1,
  2: pet2,
};
 
interface InvalidIndexSignature {
  [x: string]: Pet;
  [x: number]: Animal;
  // 'number' index type 'Animal' is not assignable to 'string' index type 'Pet'.
}

타입 호환성 관점에서, Pet(name, owner)은 Animal(name)의 서브타입입니다. Animal에서 요구하는 모든 속성을 Pet이 가지고 있기 때문입니다. 따라서 숫자 인덱스 타입이 문자열 인덱스 타입에 호환되어 타입 오류가 발생하지 않습니다. 하지만 반대의 경우에는 호환되지 않기 때문에 타입 오류가 발생합니다.

참고로 위 예제에서는 숫자 키의 값을 객체 리터럴로 직접 넣지 않고, Pet 타입으로 선언한 변수를 거쳐 할당했습니다. 객체 리터럴을 곧바로 넣으면 뒤에서 살펴볼 초과 속성 검사가 동작해, owner가 문자열 인덱서의 값 타입인 Animal에 없는 속성으로 잡히기 때문입니다.

인덱스 시그니처는 다른 속성에도 영향을 미친다

interface IndexSignatureAffectionTest1 {
  [x: number]: string;
  1: string[];
  // Property '1' of type 'string[]' is not assignable to 'number' index type 'string'.
  length: number;
}
 
interface IndexSignatureAffectionTest2 {
  [x: string]: string;
  1: string[];
  // Property '1' of type 'string[]' is not assignable to 'string' index type 'string'.
  length: number;
  // Property 'length' of type 'number' is not assignable to 'string' index type 'string'.
}

인덱스 시그니처는 다른 속성에도 영향을 미칩니다. 인덱스 시그니처가 선언되어 있으면, 명시적으로 선언한 모든 명명 속성(1, length 등)은 해당 객체에 적용되는 인덱스 시그니처의 값 타입과 호환되어야 합니다. 첫 번째 예제에서는 1이 숫자 인덱서의 값 타입인 string과 호환되지 않아 타입 오류가 발생합니다. 두 번째 예제에서는 1과 length가 문자열 인덱서의 값 타입인 string과 호환되지 않아 타입 오류가 발생합니다.

인덱스 시그니처에 readonly 키워드 사용

인덱스 시그니처에도 readonly 키워드를 사용할 수 있습니다. 이 경우, 속성을 추가하거나 수정할 수 없게 됩니다.

interface ReadonlyIndexSignature {
  readonly [x: string]: string;
}
 
const readonlyIndexSignature: ReadonlyIndexSignature = {
  name: 'Tardis',
  color: 'blue',
};
 
readonlyIndexSignature.owner = 'David Tennant';
// Index signature in type 'ReadonlyIndexSignature' only permits reading.

초과 속성 검사(Excess Property Checks)

어디서 어떻게 객체에 타입이 할당되었는지에 따라, 타입 시스템의 동작이 달라질 수 있습니다. 이러한 차이의 대표적인 예시로 초과 속성 검사(Excess Property Checks)가 있습니다.

interface Rocket {
  name?: string;
  color?: string;
}
 
function literalExample(rocket: Rocket) {
  console.log(`name: ${rocket.name ?? 'unknown'}, color: ${rocket.color ?? 'unknown'}`);
}
 
literalExample({ name: 'Castle Bravo I' }); // OK
 
literalExample({ name: 'Castle Bravo II', colour: 'Red' });
// Object literal may only specify known properties,
// but 'colour' does not exist in type 'Rocket'. Did you mean to write 'color'?
 
const castleBravo = { name: 'Castle Bravo II', colour: 'Red' };
literalExample(castleBravo); // OK - 초과 속성 검사가 발생하지 않음

문제 상황

name, color 둘 다 옵셔널 속성이므로, 해당 속성이 없어도 컴파일 오류가 발생하지 않습니다. 하지만 객체 리터럴을 직접 전달하는 경우, Rocket에 없는 colour 속성이 있다는 이유로 오류가 발생합니다.

한편, 똑같은 속성 값을 가지는 객체를 변수에 먼저 할당한 후 전달하면 오류가 발생하지 않습니다.

초과 속성 검사를 하는 경우

이렇게 동작하는 이유는, 대상 타입이 명시된 자리에 객체 리터럴이 직접 올 때(변수 할당 또는 함수 인자 전달)만 초과 속성 검사를 하기 때문입니다.

타입스크립트 핸드북에 따르면, 타입스크립트는 객체 리터럴을 (선언된 타입을 가진) 변수에 할당하거나 함수 인자로 전달할 때, 대상 타입에 없는 초과 속성을 가지고 있으면 버그일 수도 있다고 판단한다고 합니다.

literalExample에 객체 리터럴을 직접 넘긴 경우는 함수 인자 전달에 해당하므로 검사가 동작합니다. 반면 castleBravo는 타입을 명시하지 않아 추론된 타입에 colour가 그대로 포함되고, 이후 Rocket 파라미터로 전달될 때는 구조적 호환성만 확인하므로 통과합니다.

상황초과 속성 검사
객체 리터럴을 직접 할당/인자 전달검사함
변수에 할당된 객체를 전달검사하지 않음

초과 속성 검사를 피하는 방법

초과 속성 검사를 하더라도 컴파일 오류가 발생하지 않게 하려면, 인덱스 시그니처를 사용하면 됩니다.

interface Rocket2 {
  name?: string;
  color?: string;
  [propertyName: string]: unknown;
}
 
function literalExample2(rocket: Rocket2) {
  console.log(`name: ${rocket.name ?? 'unknown'}, color: ${rocket.color ?? 'unknown'}`);
}
 
literalExample2({ name: 'Castle Bravo I' }); // OK
literalExample2({ name: 'Castle Bravo II', colour: 'Red' }); // OK
 
const castleBravo2 = { name: 'Castle Bravo II', colour: 'Red' };
literalExample2(castleBravo2); // OK

인덱스 시그니처 [propertyName: string]: unknown을 추가하면, 선언하지 않은 속성도 허용하도록 타입을 정의할 수 있습니다.


정리

이번 포스트에서는 인덱스 시그니처와 초과 속성 검사에 대해 알아보았습니다. 내용을 정리하면 다음과 같습니다.

개념설명
인덱스 시그니처속성 이름을 모르지만 타입을 아는 경우 사용
복수 인덱서 규칙숫자 인덱서의 값은 문자열 인덱서 값의 서브타입이어야 함
초과 속성 검사대상 타입이 명시된 자리에 객체 리터럴이 직접 올 때 발생
인덱스 시그니처 활용초과 속성 검사를 우회할 수 있음
  • 인덱스 시그니처를 사용하면 동적인 속성을 가진 객체 타입을 표현할 수 있다.
  • 인덱스 시그니처는 다른 속성에도 영향을 미치며, 복수 인덱서 사용 시 서브타입 규칙을 따라야 한다.
  • 초과 속성 검사는 대상 타입이 명시된 자리에 객체 리터럴이 직접 올 때(변수 할당 또는 함수 인자 전달) 발생하며, 이는 타이핑 실수로 인한 버그를 방지하기 위한 것이다.

긴 글 읽어주셔서 감사합니다. 다음 포스트에서는 type과 interface의 차이점과 Mapped Type에 대해 알아보겠습니다.