타입스크립트의 핵심 원칙 중 하나는 타입의 이름이 아닌 구조를 기반으로 타입 호환성을 결정한다는 것입니다. 이걸 "덕 타이핑" 또는 "구조적 타이핑"이라고 부르기도 하죠.
그런데, 타입스크립트를 사용하다가 이 원칙이 작동하지 않는 경우가 있다는 걸 최근에 알았습니다.
타입스크립트를 사용하면서 꼭 알아야 하는 값의 집합과 구조적 타이핑의 개념부터 가볍게 설명하면서 시작해 볼게요.
값의 집합과 구조적 타이핑
TypeScript에서 값의 집합은 특정 타입이 가질 수 있는 모든 값들의 모임을 의미해요. 예를 들어, 아래와 같은 인터페이스 Person
이 있다고 해볼게요.
interface Person {
name: string;
}
Person
타입은 name
이라는 string
속성을 가진 모든 객체들의 집합이에요. 따라서 다음과 같은 객체들은 모두 Person
타입에 할당될 수 있어요.
let person1: Person = { name: "Alice" }; // ✅ OK
let person2: Person = { name: "Bob", age: 30 }; // ✅ OK
let person3: Person = { name: "Charlie", address: "123 Street" }; // ✅ OK
여기서 person2
와 person3
은 name
속성 외에 추가적인 속성을 가지고 있지만, 구조적 타이핑 덕분에 Person
타입에 할당이 가능합니다. TypeScript는 객체의 구조가 인터페이스에 부합하면 타입 호환을 인정해요.
객체 리터럴로 직접 할당할 때의 엄격한 검사
하지만 객체 리터럴을 직접 할당할 때는 이야기가 달라집니다. TypeScript는 객체 리터럴을 직접 할당할 때 더 엄격한 객체 리터럴 할당 검사(Stricter object literal assignment checks)를 수행해요.
interface Person {
name: string;
}
let person: Person = { name: "Dave", age: 25 }; // ❌ 오류 발생!
위 코드에서는 Person
타입에 정의되지 않은 age
속성이 있기 때문에 오류가 발생합니다.
// Object literal may only specify known properties, and 'age' does not exist in type 'Person'.
왜 객체 리터럴에만 엄격한 검사가 적용될까요?
TypeScript는 객체 리터럴을 직접 할당할 때 개발자가 실수로 잘못된 속성을 추가했을 가능성을 고려해요. 특히, 속성 이름의 오타나 불필요한 속성을 사전에 잡아내기 위해서죠.
예를 들어:
let person: Person = { nmae: "Eve" }; // ❌ 오류 발생!
여기서 'nmae'
는 'name'
의 오타인데, 초과 프로퍼티 검사가 없다면 이 오류를 놓칠 수 있어요. 객체 리터럴에 대해서만 엄격한 검사를 적용함으로써 이런 실수를 미리 방지하는 거예요.
반면에, 객체를 변수에 담아서 할당하면 초과 프로퍼티 검사가 적용되지 않습니다.
const personData = { name: "Frank", age: 40 };
let person: Person = personData; // ✅ OK
이는 personData
가 다른 곳에서 유효하게 생성된 객체라고 판단하기 때문이에요. 따라서 추가적인 속성이 있어도 구조적으로 호환되면 할당을 허용합니다.
구조적 타이핑이 적용되지 않는 이유
객체 리터럴로 직접 할당할 때 구조적 타이핑이 적용되지 않는 이유는 TypeScript가 코드의 안전성과 오류 방지를 위해 더 엄격한 검사를 수행하기 때문입니다. 이를 통해 개발자는 의도치 않은 속성 추가나 오타로 인한 버그를 사전에 방지할 수 있어요.
이러한 엄격한 검사는 TypeScript 1.6 버전에서 도입되었어요. 개발자가 객체 리터럴을 사용할 때 실수로 잘못된 속성을 추가하는 것을 방지하고, 타입 시스템의 일관성을 유지하기 위해서라고 해요.
초과 프로퍼티 검사를 우회하는 방법
만약 객체 리터럴로 직접 할당하면서 추가적인 속성을 포함하고 싶다면, 다음과 같은 방법을 사용할 수 있어요.
1. 타입 단언(Type Assertion)을 사용하기: 이 방법은 TypeScript에게 "내가 이 객체의 타입을 잘 알고 있으니 초과 프로퍼티 검사를 무시해달라"고 말하는 거에요.
let person: Person = { name: "Grace", age: 28 } as Person; // ✅ OK
2. 객체를 변수에 담아서 할당하기: 이렇게 하면 초과 프로퍼티 검사가 적용되지 않으므로, 추가적인 속성이 있어도 할당이 가능합니다.
const personData = { name: "Hannah", age: 35 };
let person: Person = personData; // ✅ OK
3. 인덱스 시그니처(Index Signature)를 타입에 추가하기: 인덱스 시그니처를 사용하면 객체가 임의의 속성을 가질 수 있게 되어 초과 프로퍼티 검사가 무력화됩니다. 하지만 이 방법은 타입의 안정성을 떨어뜨릴 수 있으니 신중하게 사용해야 해요.
interface Person {
name: string;
[key: string]: any;
}
let person: Person = { name: "Ian", age: 22 }; // ✅ OK
정리
- TypeScript의 구조적 타이핑은 타입의 구조를 기반으로 타입 호환성을 결정하는 핵심 원칙입니다.
- 객체 리터럴을 직접 할당할 때는 초과 프로퍼티 검사가 적용되어 구조적 타이핑이 적용되지 않습니다.
- 이는 개발자의 실수(오타, 불필요한 속성 추가 등)를 방지하기 위한 안전장치입니다.
- 변수에 담아서 할당하거나 타입 단언을 사용하면 초과 프로퍼티 검사를 우회할 수 있습니다.
- 인덱스 시그니처를 사용하면 임의의 속성을 허용할 수 있지만, 타입 안정성이 떨어질 수 있으니 신중히 사용해야 합니다.