TypeScript - 제네릭 제약: 유연한 타입의 힘을 발휘하다
안녕하세요, 미래의 TypeScript 마법사 여러분! 오늘 우리는 제네릭 제약의 세계로 흥미로운 여정을 떠납니다. 프로그래밍 초보자라도 걱정하지 마세요 - 저는 친절한 안내자가 되어 이 주제를 단계별로 설명해 드릴게요. 이 튜토리얼의 끝을 맺을 때쯤에는 제네릭을 제약하는 마스터가 될 것입니다!
제네릭 제약이란?
먼저, 간단한 비유로 시작해보겠습니다. 마법의 상자가 있다고 상상해봅시다. 이 상자는 어떤 종류의 아이템도 담을 수 있습니다. TypeScript에서 제네릭은 바로 이런东西입니다 - 다양한 타입을 담을 수 있는 유연한 컨테이너입니다. 그런데 이 상자에 들어갈 수 있는 것에 대해 어떤 규칙을 두고 싶다면 어떻게 하나요? 그게 바로 제네릭 제약입니다!
제네릭 제약은 우리가 제네릭과 함께 사용할 수 있는 타입을 제한할 수 있게 해줍니다. 마법의 상자에 "length" 프로퍼티가 있는 오브젝트만 허용한다는 레이블을 붙이는 것과 같은 이야기입니다!
문제 예제: 왜 제네릭 제약이 필요한가?
제네릭 제약이 구원의 길을 열 수 있는 몇 가지 시나리오를 살펴보겠습니다.
예제 1: 수수께끼 같은 'length' 프로퍼티
function getLength<T>(item: T): number {
return item.length; // 오류: 'T' 타입에 'length' 프로퍼티가 없습니다
}
아이고! TypeScript가 오류를 보여줍니다. 왜 그럴까요? 모든 타입이 'length' 프로퍼티를 가지고 있지 않기 때문입니다. 이 함수에 숫자를 전달하면 어떻게 되나요? 숫자는 길이가 없습니다!
예제 2: 혼란스러운 비교
function compareValues<T>(value1: T, value2: T): boolean {
return value1 > value2; // 오류: '>' 연산자는 'T' 타입과 'T' 타입에 적용할 수 없습니다
}
또 다른 오류! TypeScript는 T
가 >
연산자를 사용하여 비교할 수 있는지 모릅니다. 문자열을 전달하면 어떻게 되나요? 복잡한 오브젝트는요?
이 예제들은 우리가 제네릭 제약이 필요한 이유를 보여줍니다. 더 정확하고 오류 없는 코드를 작성하는 데 도움이 됩니다.
TypeScript에서 제네릭 제약의 작동 방식
이제 우리의 문제를 해결하기 위해 제네릭 제약을 어떻게 사용할 수 있는지 살펴보겠습니다.
마법의 extends
키워드
제약을 추가하려면 extends
키워드를 사용합니다. TypeScript에게 "이 타입은 적어도 이러한 프로퍼티나 기능을 가져야 한다"고 말하는 것과 같습니다!
우리의 getLength
함수를 고쳐보겠습니다:
interface Lengthwise {
length: number;
}
function getLength<T extends Lengthwise>(item: T): number {
return item.length; // 더 이상 오류가 없습니다!
}
이제 이를 설명해보겠습니다:
-
Lengthwise
라는 인터페이스를 정의하여length
프로퍼티를 가지게 합니다. -
<T extends Lengthwise>
를 사용하여 "T는 적어도 Lengthwise가 가지는 것을 가져야 한다"고 합니다. - 이제 TypeScript는
T
가 무엇이든length
프로퍼티를 가질 것이라고 알고 있습니다!
이제 시험해보겠습니다:
console.log(getLength("Hello")); // 작동합니다! 문자열은 길이가 있습니다
console.log(getLength([1, 2, 3])); // 작동합니다! 배열은 길이가 있습니다
console.log(getLength(123)); // 오류! 숫자는 길이가 없습니다
이게 멋지지 않나요? 우리는 성공적으로 제네릭을 제약했습니다!
제네릭 제약에서 타입 매개변수 사용
때로는 하나의 타입 매개변수를 다른 타입 매개변수에 기반하여 제약하고 싶습니다. "이 상자는 이미 들어있는 것과 호환되는 아이템만 담을 수 있다"고 말하는 것과 같습니다.
예제를 살펴보겠습니다:
function copyProperties<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
}
return target;
}
여기서 무엇이 일어나고 있나요?
- 우리는 두 개의 타입 매개변수
T
와U
를 가지고 있습니다. -
T extends U
는T
는U
가 가진 것을 적어도 가져야 하지만 더 많을 수 있다는 뜻입니다. - 이제 우리는
source
에서target
으로 프로퍼티를 복사할 수 있으며,target
은source
가 가진 모든 프로퍼티를 가질 것입니다.
이제 실제로 사용해보겠습니다:
interface Person {
name: string;
}
interface Employee extends Person {
employeeId: number;
}
let person: Person = { name: "Alice" };
let employee: Employee = { name: "Bob", employeeId: 123 };
copyProperties(employee, person); // 작동합니다!
copyProperties(person, employee); // 오류! Person은 employeeId를 가지지 않습니다
실질적인 응용과 최선의 관행
이제 제네릭 제약이 어떻게 작동하는지 이해했으므로, 몇 가지 실질적인 응용과 최선의 관행을 살펴보겠습니다.
- 오브젝트 타입 제약: 자주, 오브젝트 타입을 확실하게 하고 싶습니다:
function cloneObject<T extends object>(obj: T): T {
return { ...obj };
}
- 함수 타입 제약: 타입이 호출 가능한지 확인하고 싶습니다:
function invokeFunction<T extends Function>(func: T): void {
func();
}
- 특정 프로퍼티 제약: 오브젝트가 특정 프로퍼티를 가지는지 확인하고 싶습니다:
function getFullName<T extends { firstName: string; lastName: string }>(obj: T): string {
return `${obj.firstName} ${obj.lastName}`;
}
-
여러 제약:
&
연산자를 사용하여 여러 제약을 적용할 수 있습니다:
function processData<T extends number & { toFixed: Function }>(data: T): string {
return data.toFixed(2);
}
이 방법들을 요약한 표를 아래에 제시합니다:
방법 | 설명 | 예제 |
---|---|---|
오브젝트 제약 | 타입이 오브젝트인지 확인 | <T extends object> |
함수 제약 | 타입이 호출 가능한지 확인 | <T extends Function> |
특정 프로퍼티 제약 | 타입이 특정 프로퍼티를 가지는지 확인 | <T extends { prop: Type }> |
여러 제약 | 여러 제약을 결합 | <T extends TypeA & TypeB> |
결론: 제약의 힘을 받아들이다
축하합니다! 지금 당신은 TypeScript 도구箱에서 강력한 도구를 풀어냈습니다. 제네릭 제약은 유연하고 동시에 타입 안전한 코드를 작성할 수 있게 해주며, 두 가지의 장점을 모두 누릴 수 있습니다.
제네릭 제약을 마스터하려면 연습이 중요합니다. 기존 코드를 제네릭과 제약을 사용하여 리팩토링해보세요. 코드가 얼마나 깨끗하고 견고해지는지 놀라게 될 것입니다!
마지막으로, 조금의 프로그래밍 유머를 드리겠습니다: TypeScript 개발자가 왜 망하는가요? 너무 많은 제네릭 제약을 사용해서 어떤 종류의 결제도 받을 수 없기 때문입니다! ?
계속 코딩하고, 배우고, TypeScript를 즐겨보세요!
Credits: Image by storyset