제네릭 클래스와 인터페이스
클래스와 인터페이스에 타입 매개변수를 두어 저장한 값과 메서드의 입출력을 한 타입으로 잇는 방법을 Box와 키-값 쌍, 콜백 인터페이스로 익히고, 제약을 붙인 저장소에서 조회 실패와 참조 저장이 어떻게 드러나는지 실행 결과로 확인합니다.
제네릭 함수는 호출할 때마다 타입 매개변수가 정해졌습니다. 그런데 값을 담아 두고 여러 메서드로 넣고 꺼내는 클래스라면, 한 번 정한 타입이 그 객체의 모든 메서드에 함께 적용되어야 합니다. 숫자를 담기로 한 상자에 나중에 문자열을 넣을 수 있으면 안 되기 때문입니다.
이 글은 클래스와 인터페이스에 타입 매개변수를 두어, 저장한 값과 메서드의 입력과 출력을 한 타입으로 잇는 방법을 봅니다. 마지막으로 제약을 붙인 저장소 클래스를 만들고, 타입이 보장하는 것과 보장하지 않는 것을 실행 결과로 확인합니다. 예제는 ts-first 프로젝트에 generic-classes.ts를 만들고 include를 그 파일로 바꾸어 확인합니다.
제네릭 클래스
클래스 이름 뒤에 <T>를 적으면, 그 클래스의 속성과 메서드에서 T를 쓸 수 있습니다. 인스턴스를 만들 때 T가 정해지고, 그 인스턴스의 모든 멤버에 같은 타입이 적용됩니다.
class Box<T> {
private _value: T;
constructor(value: T) {
this._value = value;
}
getValue(): T {
return this._value;
}
setValue(newValue: T): void {
this._value = newValue;
}
isSameAs(otherValue: T): boolean {
return this._value === otherValue;
}
}
const numberBox = new Box<number>(123);
console.log(numberBox.getValue());
numberBox.setValue(456);
console.log(numberBox.getValue(), numberBox.isSameAs(456));
const inferredBox = new Box("Hello");
console.log(inferredBox.getValue().toUpperCase());123
456 true
HELLOnew Box<number>(123)으로 T를 number로 정하면, _value와 setValue의 인수, getValue의 반환값이 모두 number가 됩니다. 그래서 numberBox.setValue("abc")는 Argument of type 'string' is not assignable to parameter of type 'number'.로 진단됩니다. 타입 인수를 적지 않은 new Box("Hello")는 생성자의 인수에서 T를 string으로 추론하므로, 꺼낸 값에 문자열의 메서드를 쓸 수 있습니다.
제네릭 타입 매개변수 전파 — 상태 추적
Box<number>의 생성·setValue·getValue에서 같은 _value 저장 값이 바뀌는 실제 코드가 있다. 타입 관계 표를 남기고 number/string의 서로 독립적인 인스턴스 상태를 보강할 수 있다.
TYPE / RUNTIME / CONTRACT
한 타입 인자가 저장 값과 입출력의 타입을 연결한다
Box의 T를 number로 선택하면 저장 값, setValue의 인자, getValue의 결과가 모두 number입니다. 서로 다른 타입 인자가 별도 JavaScript 클래스를 만들지는 않습니다.
화살표는 number라는 타입 인자가 저장 값과 두 메서드의 타입을 연결함을 나타냅니다. setValue와 getValue가 서로를 호출한다는 실행 순서 그림은 아닙니다.
BOX
상태 · 입력 · 반환
numberBox는 123을 저장하고 setValue(456) 뒤 isSameAs(456)가 true입니다. setValue("abc")는 이 인스턴스의 타입 계약에 맞지 않습니다.
KEYVALUEPAIR
두 타입의 역할을 분리
KeyValuePair<K, V>는 key:K와 value:V입니다. DataStore<T>는 KeyValuePair<string, T>를 구현해 키를 문자열로 고정합니다.
PROCESSCALLBACK
입력과 출력을 따로 정한다
ProcessCallback<string, string>은 "hello" → "HELLO"입니다. <number[], number>는 [1,2,3,4,5] → 15입니다.
JAVASCRIPT
타입 인자는 실행 시 소거
Box의 생성자 구현은 하나입니다. 클래스의 T는 인스턴스 쪽 타입이며 static 멤버는 이 T를 직접 사용할 수 없습니다.
Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| numberBox 생성 | _value: 123 / T: number | 123을 같은 numberBox 객체에 저장한다 | 타입 인자는 넣고 꺼낼 값의 관계를 검사한다 |
| setValue(456) | _value: 456 / stringBox: 독립 상태 | 현재 _value를 456으로 교체한다 | 같은 Box 정의의 인스턴스들이 한 저장값을 공유하지 않는다 |
| 현재 값과 동등성 | getValue: 456 / isSameAs(456): true | getValue는 456이고 isSameAs(456)는 true다 | 다른 확장 객체 사례는 score 10→12의 공유 참조로 별도 비교한다 |
제네릭 타입 매개변수 전파 — 상태 추적
Box<number>의 생성·setValue·getValue에서 같은 _value 저장 값이 바뀌는 실제 코드가 있다. 타입 관계 표를 남기고 number/string의 서로 독립적인 인스턴스 상태를 보강할 수 있다.
TYPE / RUNTIME / CONTRACT
한 타입 인자가 저장 값과 입출력의 타입을 연결한다
Box의 T를 number로 선택하면 저장 값, setValue의 인자, getValue의 결과가 모두 number입니다. 서로 다른 타입 인자가 별도 JavaScript 클래스를 만들지는 않습니다.
화살표는 number라는 타입 인자가 저장 값과 두 메서드의 타입을 연결함을 나타냅니다. setValue와 getValue가 서로를 호출한다는 실행 순서 그림은 아닙니다.
BOX
상태 · 입력 · 반환
numberBox는 123을 저장하고 setValue(456) 뒤 isSameAs(456)가 true입니다. setValue("abc")는 이 인스턴스의 타입 계약에 맞지 않습니다.
KEYVALUEPAIR
두 타입의 역할을 분리
KeyValuePair<K, V>는 key:K와 value:V입니다. DataStore<T>는 KeyValuePair<string, T>를 구현해 키를 문자열로 고정합니다.
PROCESSCALLBACK
입력과 출력을 따로 정한다
ProcessCallback<string, string>은 "hello" → "HELLO"입니다. <number[], number>는 [1,2,3,4,5] → 15입니다.
JAVASCRIPT
타입 인자는 실행 시 소거
Box의 생성자 구현은 하나입니다. 클래스의 T는 인스턴스 쪽 타입이며 static 멤버는 이 T를 직접 사용할 수 없습니다.
Box<number>는 _value:number, setValue(number), getValue():number를 연결한다. 제네릭 인터페이스는 속성과 콜백의 입력 및 출력을 연결한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| numberBox 생성 | _value: 123 / T: number | 123을 같은 numberBox 객체에 저장한다 | 타입 인자는 넣고 꺼낼 값의 관계를 검사한다 |
| setValue(456) | _value: 456 / stringBox: 독립 상태 | 현재 _value를 456으로 교체한다 | 같은 Box 정의의 인스턴스들이 한 저장값을 공유하지 않는다 |
| 현재 값과 동등성 | getValue: 456 / isSameAs(456): true | getValue는 456이고 isSameAs(456)는 true다 | 다른 확장 객체 사례는 score 10→12의 공유 참조로 별도 비교한다 |
위 그림은 numberBox가 123을 담았다가 456으로 바뀌는 과정을 단계별로 보여 줍니다. 타입 인수 하나가 저장한 값과 메서드의 입출력을 함께 묶는 것이며, Box<number>와 Box<string>이 서로 다른 자바스크립트 클래스를 만드는 것은 아닙니다. 컴파일한 코드에는 T가 지워진 Box 클래스 하나만 남습니다.
T는 담을 값의 타입을 정할 뿐, 값을 어떻게 비교하거나 복사할지는 정하지 않습니다. 객체를 담으면 그 차이가 드러납니다.
const originalItem = { score: 10 };
const itemBox = new Box(originalItem);
console.log(itemBox.isSameAs(originalItem));
console.log(itemBox.isSameAs({ score: 10 }));
originalItem.score = 12;
console.log(itemBox.getValue().score);true
false
12isSameAs의 ===는 객체의 내용이 아니라 같은 객체인지를 비교하므로, 내용이 같은 새 객체 { score: 10 }과는 false입니다. 생성자는 객체를 복사하지 않고 받은 참조를 그대로 저장했으므로, 원래 변수로 score를 바꾸면 상자 안에서도 12가 보입니다. 복사할지, 내용으로 비교할지, 바꾸지 못하게 할지는 제네릭이 아니라 클래스의 구현이 정합니다.
제네릭 인터페이스
인터페이스에도 타입 매개변수를 둘 수 있습니다. 다음 KeyValuePair<K, V>는 키와 값의 타입을 각각 받아, 쓰는 곳마다 다른 조합의 쌍을 나타냅니다.
interface KeyValuePair<K, V> {
key: K;
value: V;
}
class DataStore<T> implements KeyValuePair<string, T> {
constructor(public key: string, public value: T) {}
print(): void {
console.log(`Key: ${this.key}, Value: ${this.value}`);
}
}
const pair: KeyValuePair<number, boolean> = { key: 1, value: true };
const userStore = new DataStore("user_id", "u12345");
const productStore = new DataStore("product_code", 98765);
console.log(pair.key, pair.value);
userStore.print();
productStore.print();1 true
Key: user_id, Value: u12345
Key: product_code, Value: 98765DataStore<T>는 KeyValuePair<string, T>를 구현하므로 키는 언제나 문자열이고, 값의 타입은 인스턴스마다 정해집니다. userStore는 DataStore<string>, productStore는 DataStore<number>로 추론되며, 클래스의 타입 매개변수를 구현할 인터페이스의 타입 인수로 그대로 넘긴 것입니다.
인터페이스의 호출 시그니처에 타입 매개변수를 쓰면, 입력과 출력의 타입을 쓰는 곳마다 정할 수 있는 함수 타입이 됩니다.
interface ProcessCallback<T, V> {
(data: T): V;
}
const upper: ProcessCallback<string, string> = (input) => input.toUpperCase();
const sum: ProcessCallback<number[], number> = (values) => values.reduce((total, value) => total + value, 0);
console.log(upper("hello"), sum([1, 2, 3, 4, 5]));HELLO 15ProcessCallback<number[], number>로 정하면 매개변수 values가 문맥으로 number[]가 되고, 반환값은 number여야 합니다. 같은 인터페이스로 문자열을 바꾸는 함수와 숫자를 모으는 함수를 모두 나타낼 수 있습니다.
더 깊이: 타입 인수가 다른 제네릭 타입끼리의 할당과 변성
Dog가 Animal을 확장할 때, Getter<Dog>를 Getter<Animal> 자리에 넣을 수 있는지는 T가 어느 자리에 쓰이는지에 달려 있습니다. TypeScript 4.7 릴리스 노트의 예로 보면, 값을 돌려주는 type Getter<T> = () => T는 Dog가 Animal에 할당되는 방향과 같은 방향으로 할당되므로 공변(covariant)입니다. 값을 받는 type Setter<T> = (value: T) => void는 방향이 뒤집혀 Animal이 Dog에 할당되어야 하므로 반공변(contravariant)이고, 두 자리에 모두 쓰면 T가 같아야만 서로 바꿀 수 있는 불공변(invariant)이 됩니다. 이렇게 타입 인수의 관계가 제네릭 타입의 관계로 이어지는 방식을 변성(variance)이라고 합니다.
구조적 타입 시스템에서는 원래 변성을 따로 따질 필요가 없습니다. 타입 인수를 넣어 본 뒤 멤버를 하나씩 비교하면 되기 때문입니다. 그런데 릴리스 노트에 따르면 컴파일러는 최적화를 위해 타입 매개변수의 변성을 미리 계산해 두고, 큰 타입을 비교할 때 구조 전체 대신 타입 인수만 비교합니다. 이 계산은 순환하는 타입에서 정확한 답을 내지 못할 수 있어서, 릴리스 노트는 오류여야 할 할당이 통과하는 예도 보여 줍니다.
4.7부터는 Getter<out T>, Setter<in T>, State<in out T>처럼 변성을 직접 적을 수 있습니다. 적은 표시가 실제 구조와 맞지 않으면, 예를 들어 값을 받는 메서드가 있는데 out만 적으면 컴파일러가 진단합니다. 다만 필요한 것보다 엄격하게 적는 것은 막지 않으므로, 릴리스 노트는 모든 매개변수에 붙이기보다 깊게 재귀하는 타입을 다루는 라이브러리처럼 정확도와 검사 속도가 문제가 될 때 신중하게 쓰라고 권합니다.
제약을 붙인 저장소
클래스의 타입 매개변수에도 앞 글의 제약을 붙일 수 있습니다. 다음 저장소는 id 속성이 있는 객체만 다루므로, 어떤 모델이든 id로 찾을 수 있습니다.
interface HasId {
id: string | number;
}
class IdentifiableRepository<T extends HasId> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: string | number): T | undefined {
return this.items.find((item) => item.id === id);
}
}
interface User extends HasId {
name: string;
email: string;
}
interface Product extends HasId {
name: string;
price: number;
}
const userRepository = new IdentifiableRepository<User>();
userRepository.add({ id: 1, name: "Alice", email: "[email protected]" });
console.log(userRepository.findById(1));
const productRepository = new IdentifiableRepository<Product>();
productRepository.add({ id: "P001", name: "Laptop", price: 1200 });
console.log(productRepository.findById("P001"));{ id: 1, name: 'Alice', email: '[email protected]' }
{ id: 'P001', name: 'Laptop', price: 1200 }IdentifiableRepository<User>에서는 add가 User만 받고 findById가 User | undefined를 돌려주므로, 저장하는 값과 찾는 값의 타입이 한 모델로 이어집니다. id가 없는 타입으로 만든 new IdentifiableRepository<string>()은 Type 'string' does not satisfy the constraint 'HasId'.로 진단됩니다.
제네릭 저장소 계약 — 상태 추적
add로 받은 참조를 배열에 저장하고 findById가 ===로 첫 일치 참조 또는 undefined를 반환한다. 추가/검색 상태와 숫자·문자열 ID 차이를 실제 repository 배열로 추적할 수 있다.
TYPE / RUNTIME / CONTRACT
저장소의 T가 추가할 값과 검색 결과를 연결한다
실제 IdentifiableRepository는 id가 있는 T를 저장합니다. 같은 클래스에 User 또는 Product를 선택하면 add의 인자와 findById의 결과가 그 모델로 연결됩니다.
그림은 add 뒤 findById를 호출한 한 번의 조회입니다. 모든 항목 중 엄격한 ID 비교로 일치하는 첫 항목을 찾거나 undefined를 반환하며, 항목 하나의 불일치만으로 검색을 끝내지 않습니다.
USER
숫자 식별자를 저장
User는 id:1, name:"Alice", email을 가진 객체입니다. add 뒤 findById(1)은 이 객체를 찾습니다.
PRODUCT
문자열 식별자를 저장
Product는 id:"P001", name:"Laptop", price:1200입니다. findById("P001")은 Product | undefined입니다.
MISSING
찾지 못한 경우를 포함
findById는 배열의 find와 엄격한 동등성 ===를 사용합니다. 값이 없으면 undefined이며 숫자 1과 문자열 "1"은 같은 식별자로 취급하지 않습니다.
STATIC CONTRACT
저장소 구현은 직접 작성
T extends HasId는 string 같은 잘못된 모델을 제한합니다. 실제 클래스의 add와 findById가 실행 동작을 담당하며 제약이 검증기나 영속 저장소를 생성하지 않습니다.
이 코드에는 별도 Repository 인터페이스나 MemoryRepository 구현체가 없습니다. HasId는 식별자 모양의 계약입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| add | items: 추가 순서 T 배열 / 참조: 받은 객체 유지 | repository가 받은 T의 참조를 items 배열에 넣는다 | add는 객체의 깊은 복사가 아니다 |
| ID 검색 | 비교: string/number 구별 / 현재: 첫 일치 후보 | findById가 ===로 순서대로 비교한다 | 숫자 1과 문자열 1은 자동으로 같은 ID가 되지 않는다 |
| 반환 | 있음: 저장한 같은 참조 / 없음: undefined | 첫 일치 참조 또는 undefined를 얻는다 | 검색이 새 객체를 만들거나 모든 중복 항목을 반환하지 않는다 |
제네릭 저장소 계약 — 상태 추적
add로 받은 참조를 배열에 저장하고 findById가 ===로 첫 일치 참조 또는 undefined를 반환한다. 추가/검색 상태와 숫자·문자열 ID 차이를 실제 repository 배열로 추적할 수 있다.
TYPE / RUNTIME / CONTRACT
저장소의 T가 추가할 값과 검색 결과를 연결한다
실제 IdentifiableRepository는 id가 있는 T를 저장합니다. 같은 클래스에 User 또는 Product를 선택하면 add의 인자와 findById의 결과가 그 모델로 연결됩니다.
그림은 add 뒤 findById를 호출한 한 번의 조회입니다. 모든 항목 중 엄격한 ID 비교로 일치하는 첫 항목을 찾거나 undefined를 반환하며, 항목 하나의 불일치만으로 검색을 끝내지 않습니다.
USER
숫자 식별자를 저장
User는 id:1, name:"Alice", email을 가진 객체입니다. add 뒤 findById(1)은 이 객체를 찾습니다.
PRODUCT
문자열 식별자를 저장
Product는 id:"P001", name:"Laptop", price:1200입니다. findById("P001")은 Product | undefined입니다.
MISSING
찾지 못한 경우를 포함
findById는 배열의 find와 엄격한 동등성 ===를 사용합니다. 값이 없으면 undefined이며 숫자 1과 문자열 "1"은 같은 식별자로 취급하지 않습니다.
STATIC CONTRACT
저장소 구현은 직접 작성
T extends HasId는 string 같은 잘못된 모델을 제한합니다. 실제 클래스의 add와 findById가 실행 동작을 담당하며 제약이 검증기나 영속 저장소를 생성하지 않습니다.
이 코드에는 별도 Repository 인터페이스나 MemoryRepository 구현체가 없습니다. HasId는 식별자 모양의 계약입니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| add | items: 추가 순서 T 배열 / 참조: 받은 객체 유지 | repository가 받은 T의 참조를 items 배열에 넣는다 | add는 객체의 깊은 복사가 아니다 |
| ID 검색 | 비교: string/number 구별 / 현재: 첫 일치 후보 | findById가 ===로 순서대로 비교한다 | 숫자 1과 문자열 1은 자동으로 같은 ID가 되지 않는다 |
| 반환 | 있음: 저장한 같은 참조 / 없음: undefined | 첫 일치 참조 또는 undefined를 얻는다 | 검색이 새 객체를 만들거나 모든 중복 항목을 반환하지 않는다 |
위 그림은 저장소가 받은 객체의 참조를 배열에 넣고, findById가 ===로 앞에서부터 비교해 처음 맞는 객체나 undefined를 돌려주는 과정을 보여 줍니다. 이 구현이 보장하지 않는 것은 다음 실행으로 드러납니다.
const practiceRepository = new IdentifiableRepository<User>();
const firstUser: User = { id: 2, name: "민지", email: "[email protected]" };
practiceRepository.add(firstUser);
practiceRepository.add({ id: 2, name: "지수", email: "[email protected]" });
console.log(practiceRepository.findById(2)?.name);
console.log(practiceRepository.findById("2"));
firstUser.name = "민";
console.log(practiceRepository.findById(2)?.name);민지
undefined
민같은 id의 사용자를 두 번 넣어도 막지 않으며, find는 처음 맞는 민지를 돌려줍니다. 숫자 2와 문자열 "2"는 ===로 같지 않으므로 두 번째 조회는 실패해 undefined이고, 그래서 결과를 쓸 때는 ?.name처럼 없는 경우를 처리해야 합니다. 저장한 객체를 복사하지 않았으므로 원래 변수로 이름을 바꾸면 저장소에서도 민이 보이며, 중복을 막거나 저장해 두는 일은 이 메모리 저장소의 범위 밖입니다. 제네릭 클래스의 문법은 공식 Handbook의 제네릭 클래스에 있습니다.
정리
- 클래스나 인터페이스에 타입 매개변수를 두면, 인스턴스나 쓰는 곳에서 정한 타입 하나가 모든 멤버에 함께 적용됩니다.
- 타입 인수를 적지 않으면 생성자의 인수에서 추론되며, 컴파일한 코드에는 타입 매개변수가 지워진 클래스 하나만 남습니다.
- 인터페이스의 호출 시그니처에 타입 매개변수를 쓰면, 입력과 출력의 타입을 쓰는 곳마다 정할 수 있는 함수 타입이 됩니다.
- 클래스의 타입 매개변수에 제약을 붙이면 그 모양을 갖춘 타입만 받으며, 맞지 않는 타입 인수는 TS2344로 진단됩니다.
- 타입 매개변수는 값의 타입만 정하므로, 복사와 비교, 중복 방지, 조회 실패의 처리는 구현이 따로 맡습니다.
연습 문제
1. 문자열 id를 더한 뒤의 조회
위의 실행에 이어 { id: "2", name: "유나", email: "[email protected]" }를 더했습니다. findById(2)?.name과 findById("2")?.name은 각각 무엇인가요? practiceRepository.findById(99).name을 바로 읽어도 되나요?
해설 보기
숫자 2로 찾으면 처음 맞는 숫자 id의 민이, 문자열 "2"로 찾으면 새로 더한 유나가 나옵니다. 두 값은 ===로 서로 다르므로, 숫자 id의 두 번째 사용자 지수는 어느 조회에서도 나오지 않습니다. 99는 없으므로 결과가 undefined이고, 반환 타입이 User | undefined여서 확인 없이 .name을 읽으면 결과가 undefined일 수 있다는 진단이 나옵니다.
2. 상자에 다른 타입 넣기
const inferredBox = new Box("Hello"); 다음에 inferredBox.setValue(42);를 쓰면 어떻게 되나요? 숫자와 문자열을 모두 담는 상자가 필요하다면 어떻게 만들어야 하나요?
해설 보기
inferredBox는 Box<string>으로 추론되었으므로, 숫자 42는 string 매개변수에 맞지 않아 진단됩니다. 두 타입을 모두 담으려면 new Box<string | number>("Hello")처럼 타입 인수에 유니온을 적어 만들며, 그러면 getValue()의 결과도 string | number가 되어 쓰기 전에 좁혀야 합니다.
3. 복사해서 저장하기
IdentifiableRepository의 add를 this.items.push({ ...item });로 바꾸면, 위 실행의 세 출력 가운데 무엇이 달라지나요?
해설 보기
마지막 출력이 민에서 민지로 바뀝니다. 전개 구문으로 만든 새 객체를 저장하므로, 원래 변수 firstUser의 이름을 바꾸어도 저장소 안의 복사본은 바뀌지 않습니다. 다만 이 복사는 한 단계만 하는 얕은 복사여서, 속성 안에 객체가 있다면 그 안쪽 객체는 여전히 함께 쓰입니다.
다음 글 조건부 타입과 제네릭에서는 5장의 조건부 타입을 제네릭과 함께 써서, 입력 타입에 따라 결과 타입이 달라지는 함수와 유틸리티를 만드는 방법을 다룹니다.