본문으로 건너뛰기

프론트엔드

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

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

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

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

객체의 표현

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

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과 호환되지 않아, 두 번째 예제에서는 1length가 문자열 인덱서의 값 타입인 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 Checking)가 있습니다.

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 둘 다 옵셔널 속성이므로, 해당 속성이 없어도 컴파일 에러가 발생하지 않습니다. 하지만 객체 리터럴을 직접 전달하는 경우, 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에 대해 알아보겠습니다.