기존에 고정된 타입을 넘어
유니온 타입을 넘어…
함수 오버로딩을 넘어…
컴포넌트를 재사용 가능하게 구축하고 싶다면?
여러가지 타입 사용이 가능한 제네릭 타입을 사용해보자!
// 제네릭 타입으로 선언한 함수
function getText<T>(text: T): T {
return text;
}
T
는 제네릭 타입 파라미터로, 함수가 호출될 때 실제 타입으로 대체됩니다.// 함수를 호출할 때 string 타입으로 선언
getText<string>('test');
// 아래와 같은 결과를 가지게 된다!
function getText<string>(text: string): string {
return text;
}
타입을 배열로 받을 경우 제네릭 타입은 T[], Array<T>
로 해주어야 합니다.
나머지 타입이나 객체의 경우 따로 처리하지 않아도 됩니다.
배열 타입만 따로 선언해야 하는 이유는 엄격한 타입 체크를 위해서 입니다.
만약 T
만으로 배열인지 아닌지를 구분한다면, 배열이 아닌 값도 전달할 수 있고, 그로 인해 예기치 못한 동작이 발생할 수 있기 때문입니다.
// 배열은 length가 있기 때문에 에러가 발생하지 않음
function getArray<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
any의 경우 함수에 어떤 값이 전달되었고 어떤 값이 반환 되었는지 알 수없지만
제네릭 타입의 경우 함수를 호출할 때 타입을 전달하기 때문에 명확히 알 수 있고 타입 추론으로 타입 체크도 가능합니다.
// 타입추론 짧은 예시
// 컴파일러에서 전달되는 인수의 값이 Number 타입인 걸 알 수 있다.
getText(123);
option의 타입이 일정하지 않다는 가정하에 아래와 같이 제네릭 타입을 설정하여 활용할 수 있습니다.
// option에는 제네릭 타입으로 선언되어 어떤 타입이든 들어올 수 있다
interface IExample<T> {
name: string;
price: number;
option: T;
}
const EX1: IExample<string> = {
name: 's20',
price: 900,
option: 'good',
}
const EX2: IExample<{ color: string; coupon: boolean }> = {
name: 's21',
price: 1000,
option: { color: 'read', coupon: false },
};
위에서 설명 했듯이 어떠한 타입을 선언하든 사용이 가능하지만 반대로 타입을 제한할 수 있는 기능도 존재합니다.
일반적으로 interface에서 extends를 사용하면 타입의 확장이 이루어 지지만 제네릭 타입의 경우 extends를 사용한 타입의 종류가 제한
되게 됩니다.
type numOrStr = number | string;
// 제네릭에 적용될 타입에 number | string 만 허용
function identity<T extends numOrStr>(p1: T): T {
return p1;
}
identity(1);
identity('a');
identity(true); // 에러!!!!!
identity([]); // 에러!!!!!
특정한 프로퍼티를 사용해야 하는 경우를 가정하면 타입의 제약 활용이 중요하다고 생각합니다.
// 아래와 같이 선언하면 에러가 발생합니다.
function getArray<T>(arg: T): T {
console.log(arg.length);
return arg;
}
위에 함수를 예시로 제네릭 타입에 어떤 타입도 들어갈 수 있는데 사람의 입장에서 생각하면 뭐든지 가능하니까 당연한거 아닐까? 생각 할 수 있지만
타입을 결정하는 컴파일러의 경우 타입을 전혀 알 수 없기 때문에 length라는 프로퍼티를 사용할 수가 없습니다.
function getArray<T>(arg: T): T {
if(typeof arg === "string" || Array.isArray(arg)) {
console.log(arg.length);
}
return arg;
}
타입 가드를 통해서 방지도 가능하지만 length가 아닌 직접 생성한 프로퍼티의 경우에는 사용이 불가능 합니다.
interface isOption{
option: number;
}
function getArray<T extends isOption>(arg: T): T {
console.log(arg.option);
return arg;
}
getArray({ option: 10, title: 'text' });
getArray(3); // 에러 발생!
제네릭 타입은 반드시 { option: number }
프로퍼티가 포함되어야 하는 제약 조건을 쉽게 선언할 수가 있습니다.
제네릭도 여러개 사용이 가능합니다.
일반적인 방법 말고 응용이 더 중요하겠죠?
// 전달 받은 객체의 value를 리턴해주는 함수
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, 'a'); // 성공
getProperty(x, 'm'); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.
// keyof 를 통해 전달 유니온 타입으로 변환된다.
function getProperty<T, K extends 'a' | 'b' | 'c' | 'd'>(obj: T, key: K) {
return obj[key];
}
전달 받는 파라미터가 콜백함수
일 경우 제네릭 타입 설정이 가능합니다.
여기서 유추 할 수 있는게 함수 자체도 타입이 될 수 있다는 점 입니다!
function translate<T extends (a: string) => number, K extends string>(x: T, y: K): number {
return x(y)
}
// 문자숫자를 넣으면 정수로 변환해주는 함수
const num = translate((a) => +a, '10')
console.log('num: ', num) // num : 10
위에서는 전달하는 파라미터를 제어해서 제약을 주는 부분을 확인했고 파라미터 뿐만 아니라 함수 타입 구조를 설정하는 것도 가능합니다.
// 제네릭 함수 타입 구조
interface GenericIdentityFn {
<T>(arg: T): T
}
const identity: GenericIdentityFn = (arg) => {
return arg
}
identity<number>(100)
함수를 할당 할때 제네릭을 결정하는 방법도 가능합니다.
interface GenericIdentityFn<T> {
(arg: T): T
const identity: GenericIdentityFn<number> = (arg) => {
return arg
}
identity(100)