🐕 제네릭
제네릭 프로그래밍은 데이터 형식에 의존하지 않고
하나의 값이 여러 다른 데이터 타입들을 가질 수 있는 기술에 중점을 두어
재사용성을 높일 수 있는 프로그래밍 방식이다.
hmm 그러니까 값은 하나지만 데이터타입은 여러개를 가질 수 있다가 핵심인가보네요
제네릭은 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 수 있게해준다고 하네요
👻what if 제네릭이 없는 세계관
function identity(arg: any): any {
return arg;
}
제네릭이 없는 세계관에서는 any 타입을 사용할 수 있습니다.
any는 어떤 타입이든 받을 수 있다는 점에서 제네릭하지만..
실제로 함수가 반환할 때 어떤 타입인지에 대한 정보는 잃게 됩니다.
number 타입을 인자로 넘겨줘도 any 타입이 반환된다는 정보만 얻게될 뿐입니다.
따라서 무엇이 반환되는지 궁금한 사람은 인수의 타입을 캡쳐할 방법이 필요하고
그 방법은 일단 any 타입을 사용하는 것은 아니겠네요
function identity<T>(arg: T): T {
return arg;
}
const identity = <T>(arg: T): T => {
return arg;
};
이거 화살표 함수에 타입을 적는게 안익숙해서 자꾸 헤매게되네요
함수의 경우엔 매개변수를 넣는 소괄호칸 근처에서 모든걸 해결한다고 일단 외우겠습니다.
identity에 <T>라는 타입 변수를 추가했습니다.
T는 유저가 준 인수의 타입을 캡처하고 이 정보를 나중에 사용할 수 있게 합니다.
위 예제에서는 우리가 캡처한 인수의 타입을 함수의 반환값으로 사용합니다.
이렇게 인수의 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있고
이를 이용해서 타입 정보를 함수의 매개변수에서 반환값으로 운반할 수 있게 만들어줍니다!
이러한 버전의 identity 함수는 타입을 불문하고 동작하니까 제네릭이라고 할 수 있습니다.
하지만 any 타입을 사용하는 것과는 다르게 반환값의 타입도 정확하게 가져갈 수 있습니다.
이렇게 제네릭 identity 함수를 작성하고 나면 두가지 방법으로 호출할 수 있어요!
let output = identity<string>('myString');
let output2 = identity("myString")
하나는 타입을 지정해주는 방법이고
하나는 타입스크립트의 타입추론을 믿는 방법입니다.
보통 두번째 방법이 일반적으로 사용된다고하네요
타입 인수 추론의 경우엔 전달하는 인수에 따라서 컴파일러가 T의 값을 자동으로 정해주게 된다고 합니다.
🥶 제네릭타입 변수 작업(Working with Generic Type Variables)
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // 오류: T에는 .length 가 없습니다.
return arg;
}
그런데 이런 경우에는 문제가 생길 수 있습니다.
우리가 정의한 T 자료형에는 length 프로퍼티가 없기 때문이에요
이런 경우엔 length 프로퍼티를 사용하고 싶은데 어떻게 하면 좋을까요?
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용합니다.
return text;
}
이렇게 작성하면 됩니다!
T[] 부분을 주목해서 살펴보면 이 제네릭 함수 코드는
T라는 변수 타입을 받은 뒤 인자 값으로는 배열 형태의 T를 받습니다.
이렇게 코드를 작성하면 text의 변수로는 배열이 들어가야합니다.
function logText<T>(text: T[]): T[] {
console.log(text.length);
return text;
}
console.log(logText([123]));
즉 이런식으로 인수를 넣어줘야 오류없이 동작하게됩니다.
그리고 배열은 항상 length 프로퍼티가 있으니
언제나 length 프로퍼티를 사용할 수 있게 됩니다.
이렇게 배열 형태를 활용하면 유연한 방식으로 제네릭에 타입을 줄 수 있습니다.
function logText<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
console.log(logText([123]));
이렇게 작성해도 똑같이 동작합니다.
🌞제네릭 인터페이스 작성해보기
interface GenericLogTextFn {
<T>(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn = logText; // Okay
interface GenericLogTextFn<T> {
(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn<string> = logText;
자유도가 높네요...
편한대로 작성하면 되겠는데 전 둘다 안편하니까 둘 중에
먼저 편해지는 방식을 채택하겠습니다.
😋제네릭에 제약 조건 주기
제네릭 함수에 어느정도 타입 힌트를 줄 수 있는 방법도 존재합니다.
만약 .length 프로퍼티정도만 허용해주고 싶은 경우엔
다음과 같이 정의를 할 수도 있습니다.
interface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
이렇게 작성하면 type을 강제하지는 않지만
length에 동작하는 인자만 넘겨받을 수 있게됩니다.
interface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
console.log(logText('123')) // okay
console.log(logText([1,2,3])) // okay
console.log(logText(123)) // error
length 프로퍼티를 가지고있는 애들만 들어갈 수 있게 되었네요!
function getProperty<T, O extends keyof T>(obj: T, key: O) {
return obj[key];
}
let obj = { a: 1, b: 2, c: 3 };
getProperty(obj, 'a'); // okay
getProperty(obj, 'z'); // error: "z"는 "a", "b", "c" 속성에 해당하지 않습니다.
객체에서도 제약조건을 넣어줄 수 있습니다.
두번째 인자로 들어갈 key 매개변수는 obj의 내부에 존재하는 속성만 들어갈 수 있습니다.
따라서 'a'는 존재하는 속성이기 때문에 들어갈 수 있으나
'z'는 존재하지 않는 속성이기 때문에 error가 발생합니다.
🐶마치며
제네릭.. 타입스크립트에서 중요하다는 말만 들었는데
이렇게만 봐도 벌써 유용해보이네요
하지만 문법이 조금 헷갈려서 제대로 쓰는데에 시간이 좀 걸릴 것 같습니다.
'typescript' 카테고리의 다른 글
reduxtoolkit 을 typescript환경에서 사용하기 (1) | 2023.04.19 |
---|---|
ts.config.json 설정 알아보기 (0) | 2023.04.18 |
리터럴 타입 니가 몬데.. (0) | 2023.04.17 |
함수를 타이핑하는 방법을 알아보자 (0) | 2023.04.17 |
타입스크립트의 인터페이스 (0) | 2023.04.17 |