제네릭은 여러 타입에서 동작하는 재사용성이 높은 컴포넌트를 만들고자 할 때 사용된다. 따라서 타입을 직접적으로 고정된 값으로 고정하기 보다는 언제든지 변할 수 있는 타입을 통해서 보다 유연하게 코드를 작성할 수 있게 해준다. 그래서 사용자는 제네릭을 사용함으로서 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있다.
add함수는 숫자를 더할 수도 문자를 더할 수도 있는 함수이다. 그래서 타입을 union을 사용해서 add함수로 넘어오는 인자들이 number와 string 타입이 모두 가능하게끔 설정을 해두었다.
function add(x: string | number, y: string | number): string | number {
return x + y;
}
add(1, 2); // 3
add('hello', 'world'); // 'helloworld'
이렇게 보면 문제가 없어 보이지만, union타입을 사용하다보면 x:string, y:string뿐만 아니라,
x:string, y:number 나 x:number, y:string 도 가능하게 된다. 이러한 경우들 때문에 컴파일러는 에러로 감지하고 add 함수의 return x+y <-- 이 부분에서 '+'연산자를 'string | number' 및 'number | string' 형식에 적용할 수 없습니다. 라는 에러를 알린다.
이를 해결하기 위한 방법으로는,
타입별로 함수를 따로 구분하는 방법이 있다. string 타입의 인자만 받는 add함수 따로, number 타입의 인자만 받는 add함수 따로 이런식으로...
function add(x: string , y: string ): string;
function add(x: number , y: number ): number;
function add(x: any, y: any){
return x + y;
}
add(1,2); // 3
add('hello', 'world'); // 'helloworld'
이러한 방식을 오버로딩이라고 한다. 이런 방식은 타입의 갯수가 늘어날수록 코드가 길어지게 되기 때문에 가독성에 좋지 않다.
다른 방법이라고 any 타입을 쓰는 것 또한 좋지 못한 방법이다. 왜냐! any라는 타입은 타입 검사를 하지 않기 때문에 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없다.
이러한 문제점과 한계를 해결하기 위해 제네릭이 있는 것이다.
generic은 함수나 클래스의 선언 시점이 아닌, 사용 시점에 타입을 선언하는 방식이다.
// 인수들을 받아서 배열로 만들어주는 메소드
function Array<T>(a: T, b: T): T[] {
return [a,b];
}
Array<number>(1,2)
Array<string>('hello','world')
Array<string | number>('ㅎ',2)
제네릭은 이런 형태로 함수의 이름 뒤에 를 붙여준다 <>안에 문자는 보통 T를 많이 사용한다. 하지만 이 안에 문자는 사용자 마음대로 지정 가능하다.
그리고 매개변수의 타입, return 반환 값 타입도 다 T로 설정해둔다. 우선 이 T가 무슨 타입인지는 모르겠지만 같은 타입은 전부 하나의 문자로 표현한다. 하지만 제네릭은 함수를 선언할 때 타입이 정해지는 게 아니라, 함수를 사용할 때 타입이 정해지기 때문에 문제없다!
그래서 함수를 사용할 때 함수의 이름 옆에 저렇게 타입을 붙여서 정해주면 된다.
제네릭 타입인 T는 어떤 타입이든 다 될 수 있다고 했지만, 그렇게 되면 범위가 너무 광범위해진다. 그래서 각 함수에 대해 사용처에 따라서 입력값을 제한해야 되는 상황이 생긴다.
function printLength<T>(arg: T): T {
console.log(arg.length)
return arg
}
위의 printLength는 에러가 뜬다. 이유는 제네릭 함수는 사용할 때 타입이 정해지기 때문에 그전에는 타입이 정의가 되지 않는다. 따라서 T타입에 length의 속성이 있는지 없는지 아직은 알 수 없기 때문에 에러가 표시 된다. 그래서 여기서 length 속성을 쓰기 위해서는 T타입에 제약 조건을 거는 방법이 있다.
interface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
logText(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' }); // `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
이렇게 extends로 조건에 제한을 해주면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 된다.
const obj = {
name: 'woony',
age: 20,
}
const obj2 = {
animal: '🐶',
}
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
console.log(getValue(obj, 'name')); // woony
console.log(getValue(obj, 'age')); // 20
console.log(getValue(obj2, 'animal')); // 🐶
T, K extends keyof T란, T의 key값들(name,age,animal)중 하나가 T,K(제네릭)가 된다는 것이다.
위의 코드에서 T는 매개변수에서 보면 obj가 T라고 되어 있는데 미리 만들어둔 obj, obj2 객체를 말한다. 그리고 매개변수key:K 는 이 객체들의 key값들을 말한다. 정리하자면, 매개변수 obj는 obj와 obj2를 타입으로 받고 key는 key값들로 타입을 받는다는 뜻이다 그래서 return 값의 타입도 T[K]로 객체의 key값을 return하겠다고 타입을 명시한 것이다.
결과를 console로 출력해보면 위의 코드의 주석처리처럼 결과를 확인할 수 있다.
출처