다형성
모든 타입은 구체 타입(concrete type)입니다.
- boolean
- string
- Date[]
- {a: number} | {b: string}
- (numbers: number[]) => number
기대하는 타입을 정확하게 알고 있고, 실제 이 타입이 전달되었는지 확인할 때는 구체 타입이 유용합니다.
하지만 때론 어떤 타입을 사용할지 미리 알 수 없는 상황이 있는데, 이런 상황에선 함수를 특정 타입으로 제한하기 어렵습니다.
JavaScript로 filter를 이용하여 배열을 반복하면서 정제하는 코드를 아래처럼 구현할 수 있습니다.
function filter(array, f){
let result = []
for(let i = 0; i < array.length; i++) {
let item = array[i]
if( f(item)){
result.push(item)
}
}
return result
}
filter([1, 2, 3, 4], _=> _ < 3) // [1, 2]로 평가
filter의 전체 타입 시그니처부터 만들어보겠습니다. 일단 타입은 unknown으로 지정합니다.
type Filter = {
(array: unknown, f: unknown) => unknown[]
}
이후 number라는 타입이라고 가정해서 unknown을 number로 바꿉니다.
type Filter = {
(array: number[], f: (item: number) => boolean) : number[]
}
위의 예시에서 배열의 타입을 number로 바꾸는 일은 그렇게 어렵지 않습니다. 하지만 filter는 범용 함수 즉 숫자, 문자, 객체, 배열, 기타 등등으로 구성된 배열을 거를 수 있어야 합니다.
문자열도 거를 수 있도록 오버로드를 이용해 함수를 확장해 보겠습니다.
type Filter = {
(array: number[], f: (item: number) => boolean) : number[]
(array: string[], f: (item: string) => boolean): string[]
}
현재까진 문제가 없어 보입니다. 다음은 객체 배열도 지원할 수 있게 확장해 보겠습니다.
type Filter = {
(array: number[], f: (item: number) => boolean) : number[]
(array: string[], f: (item: string) => boolean): string[]
(array: object[], f: (item: object) => boolean): object[]
}
얼핏 보기엔 문제가 없어 보이지만 실제 사용해보면 문제가 발생합니다.
시그니처(filter: Filter)대로 filter 함수를 구현하고 실행하면 아래의 결과가 나타납니다.
let ids = [
{ firstId: 'linda' },
{ firstId: 'andy' },
{ firstId: 'baetom' }
]
let result = filter(
ids,
_ => _.firstId.startsWith('b') // 'object' 형식에 'firstId' 속성이 없습니다.ts(2339)
)
result[0].firstId; // 'object' 형식에 'firstId' 속성이 없습니다.ts(2339)
여기서 에러가 발생하는 이유는, TypeScript에 filter로 숫자, 문자열, 객체의 배열을 전달할 것이라고 선언했고, 그다음에 객체 배열을 전달했는데, object는 객체의 실제 형태에 대해선 어떤 정보도 알려주지 않기 때문입니다.
따라서 배열에 저장된 객체의 프로퍼티에 접근하려 시도하면 TypeScript가 에러를 발생시킵니다. 배열에 저장된 객체의 형태를 알려주지 않았기 때문입니다.
에러를 해결하려면 제네릭 타입(generic type)을 이용하면 됩니다.
제네릭 타입 매개변수
여러 장소에 타입 수준의 제한을 적용할 때 사용하는 플레이스홀더 타입(placeholder type).
다형성 타입 매개변수(polymorphic type parameter)라고도 부름.
다음은 filter 예제에 제네릭 타입 매개변수 T를 적용한 코드입니다.
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
위의 코드는 "filter 함수는 T라는 제네릭 타입 매개변수를 사용하고, 지금 이 타입이 무엇인지 알 수 없으니 누군가 filter를 호출할 때마다 TypeScript가 알아서 추론해주면 좋겠다 >_<"라는 뜻입니다.
TypeScript는 전달된 array의 타입을 보고 T의 타입을 추론합니다. filter를 호출한 시점에 TypeScript가 T의 타입을 추론해내면 filter에 정의된 모든 T를 추론한 타입으로 대체합니다.
T는 자리를 맡아둔다는 의미의 '플레이스홀더' 타입이며, 타입 검사기가 문맥을 보기 이 플레이스홀더 타입을 실제 타입으로 채우는 것입니다.
이처럼 T는 filter의 타입을 매개변수화 하며, 이 때문에 T를 제네릭 타입 매개변수라고 부르는 것입니다.
꺾쇠괄호 <>로 제네릭 타입 매개변수임을 선언합니다.
꺾쇠괄호를 추가하는 위치에 따라 제네릭 타입 매개변수의 범위가 결정되며 TypeScript는 지정된 영역에 속하는 모든 제네릭 타입 매개변수 인스턴스가 한 개의 구체 타입으로 한정되도록 보장합니다.
또한 TypeScript는 filter에 어떤 인수를 넣어 호출하느냐에 따라 T를 어떤 구체 타입으로 한정할지 정합니다.
필요하면 꺾쇠괄호 안에 제네릭 타입 매개변수 여러 개를 콤마로 구분해 선언할 수 있습니다.
함수의 매개변수가 함수를 호출할 때 건네진 인수로 매번 다시 한정되듯, T도 filter를 호출할 때마다 새로운 타입으로 한정됩니다.
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter = (array, f) => {
let result = []
for(let i = 0; i < array.length; i++){
let item = array[i]
if(f(item)) {
result.push(item)
}
}
return result
}
// 1. T는 number로 한정됨
filter([1, 2, 3], _ => _ > 2)
// 2. T는 string으로 한정됨
filter(['a', 'b'], _ => _ !== 'b')
// 3. T는 {firstId: string}으로 한정됨
let ids = [
{ firstId: 'linda' },
{ firstId: 'andy' },
{ firstId: 'baetom' }
]
filter(ids, _ => _.firstId.startsWith('b'))
TypeScript는 전달된 인수의 타입을 이용해 제네릭을 어떤 타입으로 한정할지 추론합니다.
제네릭은 함수의 기능을 구체 타입을 사용할 때보다 더 일반화하여 설명할 수 있는 강력한 도구입니다. 함수 매개변수를 s: string으로 정해 매개변수 n의 값으론 string만 오도록 제한하듯 제네릭 T도 T로 한정하는 타입이 무엇이든지 모든 T를 같은 타입으로 제한합니다.
타입 별칭, 클래스, 인터페이스에서도 제네릭 타입을 사용할 수 있습니다.
가능하면 제네릭을 사용합시다.
제네릭은 코드를 일반화하고, 재사용성을 높이고, 간결하게 유지하는 데 도움을 줍니다(많이...)
'👶 TypeScript' 카테고리의 다른 글
제네릭을 어디에 선언할 수 있을까? (0) | 2023.01.13 |
---|---|
언제 제네릭 타입이 한정되는가? (0) | 2023.01.12 |
오버로드된 함수 타입 (0) | 2023.01.12 |
문맥적 타입화 (0) | 2023.01.12 |
호출 시그니처 (0) | 2023.01.12 |