에릭말고 제네릭
1. Generics
사실 저는 처음 들어봤지만, 제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 떄 자주 활용되는 특징입니다. 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용됩니다.
2. 기본 형태
function logContent<T>(content: T): T{
console.log(content);
return content;
}
logContent('soryeongk');
function logContent<T>(content: T): T{
console.log(content);
return content;
}
logContent('soryeongk');
선언 시점에 타입을 결정하는 것이 아니라 호출 시점에 타입을 결정할 수 있게 합니다.
T
는 Type을 의미하기 위해 작성한 것입니다. 이름은 아무래도 좋습니다. :)
// T를 굳이 안써도 됨
function logContent<soryeongk>(content: soryeongk): soryeongk {
console.log(content);
return content;
}
3. 왜 써요?
이상의 예시처럼 입력받은 데이터를 그대로 반환해야하는 함수가 있다고 가정해보겠습니다. string이 들어오면 string을, number가 들어오면 number를 반환해야합니다.
단순한 방식을 생각해보면 아래처럼 작성할 수 있습니다.
function logString<string>(content: string): string{
console.log(content);
return content;
}
function logNumber<number>(content: number): number{
console.log(content);
return content;
}
두 함수는 매우 비슷하게, 아니 js라면 그냥 이름만 다른 함수입니다. 동일한 기능을 하는 함수를 타입 때문에 두 번 정의하는 것은 누가봐도 비효율적입니다.
호출 시점에 타입이 정의되는 제네릭으로 함수 중복 문제를 해결할 수 있습니다.
3-1. 엥 유니온 쓰면 안되나요?
가능은 하쥬. 한 번 보시쥬.
전달받을 데이터가 무조건 string 또는 number라고 가정해보겠습니다. 아래와 같이 작성하면 argument의 타입은 string 또는 number이기만 하면 됩니다.
function logContent(content: string | number) {
console.log(content);
return content;
}
logContent('soryeongk');
logContent(25);
3-2. 엥 그럼 왜 굳이 제네릭?
지금처럼 단순한 작업말고 한가지를 더 넣어보겠습니다.
function logContent(content: string | number) {
console.log(content);
return content;
}
const name = logContent('soryeongk');
console.log(typeof name); // 결과는?
name.length // 결과는?
정답
console.log에는string
이 찍히고,name.length
는 아래와 같은error
가 납니다.
왜?
분명 name을string
으로 인식했지만, 정작string
에서 제공하는length
는 사용할 수 없습니다. 이는logContent
의 인자의 타입은string | number
이기 때문에 본 타입에는length
가 없다고 인식합니다..
3-3. 유니온 대신 제네릭 쓰기
function logContent<T>(content: T): T {
console.log(content);
return content;
}
const name = logContent('soryeongk');
console.log(name.length) // 결과는?
정답
여전히 error가 납니다. 인자로 넘어온 string의 prototype을 사용하기 위해서는 제네릭을 사용하더라도 해당 인자가 무슨 타입인지를 지정해줘야합니다. 아래와 같이 사용하실 수 있습니다.
function logContent<T>(content: T): T {
console.log(content);
return content;
}
const name = logContent<string>('soryeongk');
console.log(name.length) // 9
4. 조금 더 활용해보자!
제가 속한 스터디(스파르타스: SparTaS)의 멤버들의 정보를 저장해보려합니다.
스터디원은 령이, 졍이, 언이, 수야, 희야, 함이입니다. 령이, 졍이, 언이, 수야는 97년생, 희야는 98년생, 함이는 빠른 98년생입니다.
interface SparTaS {
name: string;
born: number;
isAbsent: boolean;
}
const ryeongE: SparTaS = { name: '령이', born: 97, isAbsent: false };
const jeongE: SparTaS = { name: '졍이', born: 97, isAbsent: false };
const eonE: SparTaS = { name: '언이', born: 97, isAbsent: false };
const heeYa: SparTaS = { name: '희야', born: 98, isAbsent: false };
// born: "빠른 98"
const suYa: SparTaS = { name: '수야', born: 97, isAbsent: true };
‘함이’를 입력하려고 하는데, 나이가 문제입니다. 함이는 25살이라고 박박 우기지만 사실 범띠이기 때문이죠.. 그래서 함이는 빠른 98
이라고 입력하고 싶은데, 나머지는 number로 입력되어있어서 string을 넣을 수가 없네요..
4-1. 해결 방법을 생각해보자!
이 때 사용가능한 방법은 아래와 같습니다.
- 함이를 위한 타입을 따로 지정한다.
함이를 SparTaS에서 제외한다.- born 부분을 유니언으로 사용한다.
- 인터페이스에 제네릭을 가미한다.
1번은 너무 비효율적이고, 2번은 간단하지만 매몰차네요,,
3번은 위에서 살펴본 것처럼 number 타입에서 제공하는 프로토타입을 사용할 수 없게 될 수도 있을 것 같아 좋아보이지 않아요.
그래서 4번의 방법으로 구현해볼게요!
4-2. 인터페이스에 제네릭 가미하기
일단 함이를 추가해봅니다. 역시 에러가 나네요..!
SparTaS의 born에만 제네릭을 사용하여 해결해볼까요?!
interface SparTaS<T> {
name: string;
born: T;
isAbsent: boolean;
}
const ryeongE: SparTaS<number> = { name: '령이', born: 97, isAbsent: false };
const jeongE: SparTaS<number> = { name: '졍이', born: 97, isAbsent: false };
const eonE: SparTaS<number> = { name: '언이', born: 97, isAbsent: false };
const heeYa: SparTaS<number> = { name: '희야', born: 97, isAbsent: false };
const suYa: SparTaS<number> = { name: '수야', born: 97, isAbsent: false };
const HamE: SparTaS<string> = { name: '함이', born: '빠른 98', isAbsent: false };
제네릭을 사용하니, 함이를 위한 새로운 타입의 정보도 알맞게 넣을 수 있고, 추후 number 나 string 본연의 기능도 사용할 수 있게 되었어요! 우와!
5. 제네릭의 타입 제한
function logTextLength<T>(text: T): T {
console.log(text.length);
return text;
}
logTextLength('hi');
function logTextLength<T>(text: T): T {
console.log(text.length);
return text;
}
logTextLength('hi');
logTextLength가 정의된 순간에는 T에 어떤 타입이 들어올지 알 수 없습니다. 이 때, 함수 내에서 text의 length를 찍어보려고 하면 오류가 발생합니다.
그래서 T 안에 string, array 등 .length를 지원하는 타입이 있을 것임을 명시해줘야합니다.
5-1. 일단 방법을 알려드리죠
방법 1.
function logTextLength<T extends string>(text: T): T {
console.log(text.length);
return text;
}
logTextLength('hi');
방법 2.
interface 활용
interface lengthType {
length: number;
}
function logTextLength<T extends lengthType>(text: T): T {
console.log(text.length);
return text;
}
logTextLength('soryeongk');
text가 전달받을 타입 중에 number 타입을 가지는 length라는 프로퍼티가 있음을 알려주는 것입니다.
5-2. superType과 subType, extends는 확장인가 제한인가
부끄럽게도
superType
과subType
이 정확한 명칭인지는 확인되지 않았습니다.. 슈퍼 뭐시기, 서브 뭐시기.. 아시는 분은 댓글 남겨주세요..!
interface Person {
name: string;
age: number;
}
interface Developer {
name: string;
age: number;
isFE: boolean;
}
위 코드는 다음과 같이 작성될 수 있습니다.
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
isFE: boolean;
}
여기서 Developer는 Person의 subType입니다. Person은 Developer의 superType입니다.
Person은 타입이 두 개밖에 없고 Developer에는 3개나 있는데?!
그래도 Person이 super이고 Developer가 sub입니다.
더 많은 제한을 가지고 있는 친구가 sub가 됩니다.
예시를 볼까요?
아래 코드에서 오류가 뜨는 것은 몇 번일까요?
let person: Person;
let developer: Developer;
person = developer; // 1번
developer = person; // 2번
정답은 2번입니다 :) 이유를 잘 모르시겠다면 댓글 남겨주세요!
5-3. keyof 활용
keyof는 Object의 key들의 literal 값들을 가져옵니다.
interface Person {
name: string;
age: number;
}
type test = keyof Person; // ('name', 'age')
이것을 제네릭 제한에 활용해보게씁니당
interface Person {
name: string;
age: number;
}
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let person: Person = {
name: 'soryeongk',
age: 25
};
let myString: string = pluck(person, 'name');
let myNumber: number = pluck(person, 'age');
console.log(myString); // 결과는?
console.log(myNumber); // 결과는?
keyof를 통해 받아올 key값을 한정하는 데 사용됩니다! :)
꿑
Author And Source
이 문제에 관하여(에릭말고 제네릭), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@soryeongk/genericsTS저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)