제네릭
제네릭이 뭐지?
function getText(text) {
return text;
}자바스크립트에서는 동적타이핑이 지원되기 때문에 위와같이 함수를 작성하고, 타입이 어떤 타입이든 간에 입력한 값을 그대로 출력한다
하지만 타입스크립트는 반드시 파라미터의 타입을 명시해야하므로, 문자열타입, 숫자, 클래스 등 자신이 원하는 타입에 대한 함수를 일일이 전부 선언해주거나 유니온타입을 활용해야 한다는 불편함이 있다
이러한 이유로 존재하는것이 제네릭(Generics) 이다
function getText<T>(text: T) {
return text;
}제네릭은 관용적으로 T로 많이 표현한다
제네릭의 특징
- 타입이 고정되는것을 방지하고, 재사용 가능한 요소를 선언할 수 있다
- 타입검사를 컴파일 시간에 진행함으로써 타입 안정성을 보장한다
- 캐스팅 관련 코드를 제거할 수 있다 (일일이 선언할 필요없다는 뜻)
챗지피티를 통해 알아본 관용 문자들
- T: 일반적인 타입 변수로, 어떤 타입이든 받을 수 있는 가장 기본적인 제네릭 타입.
- U: 두 번째 제네릭 타입으로,
T와 함께 사용되어 두 타입을 비교하거나 연관지을 때 사용.- K: 객체의 키를 나타내는 제네릭 타입으로,
keyof와 함께 자주 사용.- V: 객체의 값을 나타내는 제네릭 타입.
- E: 배열의 요소 타입을 나타내는 제네릭 타입.
- P: 함수의 매개변수 타입을 나타내는 제네릭 타입.
- R: 함수의 반환 타입을 나타내는 제네릭 타입.
챗지피티를 통해 알아본 관용 문자들
- **T**: 일반적인 타입 변수로, 어떤 타입이든 받을 수 있는 가장 기본적인 제네릭 타입.
- **U**: 두 번째 제네릭 타입으로, `T`와 함께 사용되어 두 타입을 비교하거나 연관지을 때 사용.
- **K**: 객체의 키를 나타내는 제네릭 타입으로, `keyof`와 함께 자주 사용.
- **V**: 객체의 값을 나타내는 제네릭 타입.
- **E**: 배열의 요소 타입을 나타내는 제네릭 타입.
- **P**: 함수의 매개변수 타입을 나타내는 제네릭 타입.
- **R**: 함수의 반환 타입을 나타내는 제네릭 타입.제네릭 타입
간단한 예시를 통해 위에서 제네릭타입을 소개했지만, 조금 더 자세한 예시를 통해 설명하려한다.
기본적인 사용방법
기본적으로 꺽쇠 <>기호를 변수명 앞에 쓰면 타입 단언이 되게된다 (전에 as 타입으로 타입단언을 한다고 다룬적있다)
따라서 제네릭을 사용하기 위해서는 변수명, 함수명 뒤에다가 꺽쇠 괄호를 적어주어야한다
기호는 그리고 통상적으로 T를 쓸 뿐이지 어떠한 문자가 와도 상관없다 (기본타입을 제네릭타입으로 쓰겠다고 개기면 IDE에서부터 안된다고 막힌다)
function toArray<T>(a: T, b: T): T[] {
return [a, b];
}
// 화살표함수에서 제네릭 타입 사용하는 방법
const toArray2 = <T>(a: T, b: T): T[] => {
return [a, b];
};
// 방법1: 타입명시
toArray<number>(1, 2);
toArray<string>('1', '2');
toArray<number | string>(1, '2');
// 방법2: 타입추론, 컴파일러는 전달하는 인수의 타입을 보고 스스로 추론하기 때문에 함수호출시 제네릭을 안써줘도 알아서 추론한다
toArray2(1, 2);
toArray2('1', '2');
toArray<string | number>(1, '2'); // 하지만 이와같이 타입추론이 힘든경우에는 명시를 해줘야한다제네릭 함수를 호출할 때 가독성 측면 때문에 **방법2 가 더 많이, 더 흔하게 사용**된다고 한다
하지만 **코드가 복잡**해지면(사용자 클래스, 인터페이스, 타입별칭 등) **직접 제네릭을 지정**해줘야 한다배열
function arrayGenerics<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
function arrayGenerics2<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}T[] 나 Array<T> 로 처리한다. Array에는 length 속성을 가지고 있으므로 오류가 나지않는다
그럼 만약 있는지 없는지 컴파일러가 잘 모른다면…?? (조금 있다가 살펴보자..)
제네릭과 인터페이스, 타입별칭
제네릭은 인터페이스와 굉장히 많이 쓰인다
interface Mobile<T> {
name: string;
price: number;
option: T;
}
// 제네릭타입으로 리터럴 객체타입도 할당할 수 있다
const m1: Mobile<{color: string, coupon: boolean}> = {
name: "s21",
price: 1000,
option: {color: "red", coupon: false},
}타입별칭 또한 잘 어울린다
type TG<T> = T[] | T;
const number_arr: TG<number> = [1,2,3,4];
const number_ele: TG<number> = 1234;
const string_arr: TG<string> = ["Hello", "world"];
const string_ele: TG<string> = "Helloworld";제네릭 함수타입
function logText<T>(text: T): T {
return text;
}
// 방법 1
let str: <T>(text: T) => T = logText;
// 방법 2
let str: {<T>(text: T): T} = logText;위와 같은 방식으로 함수 시그니처를 제네릭타입으로 정해줄 수 있다 (다소 생소한 표현들이니 익혀두자)
이를 인터페이스나 타입별칭으로 옮기면 다음과 같다
interface GenericLogTextFn {
<T>(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn = logText; // Okay그리고 function 키워드를 사용해 함수 선언, 정의할때는 함수시그니처를 정해줄수 없는것 처럼 보여진다. (함수선언식으로는 안되나보다)
interface GenericLogTextFn {
<T>(text: T): T;
}
const logText: GenericLogTextFn = (text) => {
return text;
}
let myString: GenericLogTextFn = logText;const나 let을 사용하면 함수표현식으로 함수를 정의할수 있는데, 이 때에는 함수 시그니처를 정해줄수 있다.
아마 호이스팅되는 것이 서로 달라서 그런거 같기도하다
위 코드에서 인터페이스에 인자타입을 강제하고 싶으면 아래와 같이 변경할 수 있다
interface GenericLogTextFn<T> {
(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn<string> = logText;다만, **이넘**(Enum)과 **네임스페이스**(namespace)는 **제네릭으로 생성할수 없다**
이유를 찾아보면 이렇다 → **목적과 맞지 않는다!**
둘다 아직 다루지 않았지만, 이후에 다룰 예정이다
이넘(Enum)은 **상수집합을 정의하는데 사용**되기 때문에, 애초에 타입명시릃 사용하지 않는다, 따라서 당연히 제네릭 타입도 지원하지 않는다
네임스페이스는 **코드의 조직화의 도구**이다, 즉 코드의 모듈화를 해줄수 있게 도와주는 애인데 이 네임스페이스에 제네릭을 사용하는것은 목적과 맞지 않는다고 한다.
하지만 오해하면 안될 것이 **네임스페이스 내부의 변수나 함수, 클래스에서는 제네릭을 사용**할 수 있다
관련된 코드들을 묶어주는 **네임스페이스 선언에서 제네릭을 사용할 수 없다**는 뜻이다제네릭 제약조건 (extends)
지금까지 잘 따라오고 있다면, 그럼 제네릭 너무 사기아닌가? 결국 자바스크립트마냥 다 타입을 다 허용해주면 결국 가독성 안좋아지고, 버그 어디서 나는지 디버깅하기 힘들어지는거 아닌가? 하는 합리적인 의심을 할 수 있다
합리적인 의심은 항상 옳다 → 나보다 백배, 천배는 우수한 개발자들이 내가 한 의심을 안했을 리가 없다 당연히 그에 대한 대응방식이 존재하거나, 어떠한 의도를 가지고 대응방식을 만들지 않거나 둘 중 하나이다
당연히 제네릭타입으로 선언할수 있는 타입을 제한해주는 방법이 존재한다 (extends)
title: 인터페이스 확장의 extends와 다름!
인터페이스에서 extends 는 현재 인터페이스 해당인터페이스를 확장하는, 상속받는 느낌이라면
**제네릭**에서 extends 는 **아예 다른 의미**이다
<T extends K> 형태의 제네릭이 있다면, `T가 K에 할당 가능해야한다`라는 제약조건을 거는 것이다type numOrStr = number | string;
function restrictMethod<T extends numOrStr>(arg: T): T {
return arg;
}
restrictMethod(123);
restrictMethod("123");
// 에러 발생
restrictMethod(true);
restrictMethod([]);
restrictMethod({});속성 제약조건
변수에 따라 제약조건을 걸기 싫은데 걸어줘야 컴파일되는 경우들이 있다. 이전에 컴파일러가 변수에 해당 속성이 존재하는지 모르는 경우 어떻게할까에 대한 답을 하려한다
function propertyCheck<T>(arg: T): T {
console.log(arg.length);
return arg;
}위와 같이 작성하면 ‘T’형식에 length 속성이 없습니다 에러가 발생한다 그러면 이전에 배운 방식대로 타입가드를 적용해보면 어떨까??
function propertyCheck<T>(arg: T): T {
if ("length" in arg) console.log(arg.length);
return arg;
}이번엔 ‘T’ 형식은 ‘object’ 형식에 할당할 수 없습니다 에러가 발생한다
in 키워드를 사용하면 해당 변수는 object로 간주하는데, T형식을 가진 변수가 object인지 아닌지 알수 없다는 의미이다
function propertyCheck<T>(arg: T): T {
if (typeof arg === 'string' || Array.isArray(arg)) console.log(arg.length);
return arg;
}타입가드를 활용하기 위해서는 위와같은 방식으로 사용해야한다
extends를 통해 보다 유연하게 코드 작성을 할 수 있다
interface LengthWise {
length: number;
}
// 제네릭 T는 반드시 {length: number} 프로퍼티 타입을 포함해야한다 (할당 가능해야한다)
function propertyCheck<T extends LengthWise>(arg: T): T {
console.log(arg.length);
return arg;
}
propertyCheck('123');
propertyCheck([1, 2, 3]);
propertyCheck({ length: 5, 0: 1, 1: 3, 2: 5, 3: 7, 4: 9 });
propertyCheck({ 0: 1, 1: 3, 2: 5, 3: 7, 4: 9 }); // 에러발생반드시 제네릭 T에 포함, 할당 가능해야하는 것을 정해줌으로써 컴파일타임에 미리 타입검사를 할 수 있다
매개변수 제약조건
하나의 함수에서 제네릭은 여러개 지정해서 사용 할 수 있다
<T, U, K…> 콤마로 구분
이를 이용하여 각 매개변수마다 다른 제네릭 타입 조건 제한을 걸 수 있다
function myFunc<T extends string, K extends number>(arg1: T, arg2: K): void {
console.log(typeof arg1);
console.log(typeof arg2);
}
myFunc('1', 2);생각보다 간단하다. 그리고 응용되는 분야는 정해져 있는듯 하다.
제네릭 타입을 하나의 함수나 클래스에서 선언 시에 여러개 사용해야할 상황이 많을까?? 내 생각은 그렇지 않다
통상적으로 사용되는 제네릭타입 문자를 T, 두번째 제네릭 타입을 U라고 정해놓고, 세번째는 따로 정하지 않은 것 또한 제네릭 타입을 많이 사용할 일이 없기 때문이라 생각한다
그리고 위 예시에서 <T extends string, K extends number> 여기서 T와 K를 아무 생각 없이 적은거 같지만 다 이유가 있다
K는 통상적으로 key, 즉 속성을 의미하는 문자로 많이 쓰인다.
keyof 키워드
keyof는 주어진 타입의 모든 키를 문자열 리터럴 타입으로 반환한다
````ad-white
title: interface
```ts
interface Person {
name: string;
age: number;
location: string;
}
// 반환: "name" | "age" | "location"
type PersonKeys = keyof Person;
```
````
````ad-white
title: type alias
```ts
type Person = {
name: string;
age: number;
location: string;
};
// 반환: "name" | "age" | "location"
type PersonKeys = keyof Person;
```
````title: generics
```ts
interface Person {
name: string;
age: number;
}
interface Korean extends Person {
liveInSeoul: boolean;
}
// type T1 = keyof Person;
function swapProperty<T extends Person, K extends keyof Person>(p1: T, p2: T, key: K): void {
// p1 객체에 있는 key의 value를 p2 객체에 있는 key의 value와 스왑하는 함수
// 그런데 당연히 p1 객체에 있는 key는 p2에도 존재해야 되기 때문에 key인수의 제네릭 K를 (keyof Person) 으로 제한
const temp = p1[key];
p1[key] = p2[key];
p2[key] = temp;
}
const p1: Korean = {
name: '홍길동',
age: 23,
liveInSeoul: true,
};
const p2: Korean = {
name: '김삿갓',
age: 31,
liveInSeoul: false,
};
swapProperty(p1, p2, 'age'); // 객체의 age 키의 값을 서로 스왑
/*
{ name: '홍길동', age: 31, liveInSeoul: true }
{ name: '김삿갓', age: 23, liveInSeoul: false }
*/
```매개변수 제약조건을 사용할 떄는 위와 같이 keyof 키워드를 활용하여 상위 인터페이스를 포함하게끔 제약을 거는 방식이나, 일종의 객체에 대한 타입가드로써 keyof와 제네릭타입을 활용한다
type Values<T> = T[keyof T]위 식은 T에 들어갈 객체의 속성들의 타입만 허용한다는 타입별칭 선언문이다. 타입 단언과 함께 쓰면 꽤 잘 활용할 수 있다는거 알아두면 좋을듯 (해당 객체에서 사용하는 타입 말고는 전부 안되게 한다는 뜻)
함수 제약조건
function translate<T extends (a: string) => number, K extends string>(x: T, y: K): number {
return x(y);
}
// 문자숫자를 넣으면 정수로 변환해주는 함수
const num = translate((a) => { return +a; }, '10');
console.log('num: ', num); // num : 10이런방식으로 제약을 걸 수 있다곤 하는데.. 굳이? 타입별칭이나, 인터페이스를 통해서 함수시그니처를 정하는게 가독성, 코드 일관성 측면에서 더 좋은 방법인거 같다
제네릭과 클래스
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(v: T, cb: (x: T, y: T) => T) {
this.zeroValue = v;
this.add = cb;
}
}
let myGenericNumber = new GenericNumber<number>(0, (x, y) => {
return x + y;
});
let myGenericString = new GenericNumber<string>('0', (x, y) => {
return x + y;
});
myGenericNumber.zeroValue; // 0
myGenericNumber.add(1, 2); // 3
myGenericString.zeroValue; // '0'
myGenericString.add('hello ', 'world'); // 'hello world'당연히 클래스에서 제네릭타입을 적용할 수 있다
const num_stack = new Stack<number>();
const string_queue = new Queue<string>();특히 자료구조와 같은 것들을 클래스로 많이 이용하는데, 이때 제네릭을 사용하면 매우 유연하게 코드를 작성할 수 있다
단, **클래스를 제네릭을 관리**할 때, **static 정적 멤버**는 제네릭으로 **관리할 수없다**
이유는 단순하다. 클래스의 인스턴스들은 각각이 다른 클래스 인스턴스와 독립적이지만 정적변수는 **서로 공유** 한다
따라서 클래스를 제네릭으로 관리한다 했을때 **해당 제네릭타입을 정적변수에 사용할 수 없는 것**이다
대신 **클래스와 독립적인 제네릭타입**을 가질순 있다.
```ts
class MyClass<T> {
static process<U>(value: U): U {
return value;
}
}
const result = MyClass.process<string>("Hello");
console.log(result); // "Hello"
```
하지만 그냥 정적 메소드나 변수에서는 제네릭을 잘 사용하지 않는 듯하다