📘 TypeScript

TypeScript - 제네릭

zunwon 2023. 11. 22. 21:15

제네릭

  • <타입변수>
  • 타입을 마치 함수의 파라미터처럼 사용하는 것
function toObj<T>(a: T, b: T): { a: T; b: T } {
  // 재사용가능
  return { a, b };
}

toObj<string>("a", "b");
toObj<number>(1, 2);
toObj(true, false);

타입변수를 명시적으로 넣어주지 않아도 된다. 그러나 첫번째 인수로 타입 추론을 하기 때문에 첫번째 인수와 타입이 다르면 에러가 발생한다

function toObj<T>(a: T, b: T): { a: T; b: T } {
  // 재사용가능
  return { a, b };
}

toObj<string>("a", 12); // 에러 발생
toObj(1, 2); // O
toObj(true, false); // O

제약조건

  • <타입변수 extends TYPE>
  • 제네릭을 더욱 안정적으로 사용할 수 있다
function toObj<T extends string | number | boolean>(a: T, b: T): { a: T; b: T } {
  // string or number or boolean만 허용
  return { a, b };
}

함수에서의 제네릭

function 함수명<타입변수>(매개변수 : 매개변수타입) {}

const 화살표함수명 = <타입변수>(매개변수 : 매개변수타입) => {} ;

인터페이스에서의 제네릭

  • interface 인터페이스명<타입변수>
interface ToObj<T> {
  a: T;
  b: T;
}

function toObj<T extends string | number | boolean>(a: T, b: T): ToObj<T> {
  return { a, b };
}

타입별칭에서의 제네릭

interface User<T, U, V> {
  name: T;
  age: U;
  isValid: V;
}

const heropy: User<string, number, boolean> = { name: "heropy", age: 30, isValid: true };
const neo: User<string, number, boolean> = { name: "neo", age: 55, isValid: true };
const amy: User<string, number, boolean> = { name: "amy", age: 20, isValid: false };

위의 코드와 같이 객체 형태가 아니라 배열 형태로 바꾸기 위해 타입 별칭이 사용된다.

  • 인터페이스에서 배열 타입은 인덱스 시그니처를 사용해야만 하기 때문에 타입 별칭을 사용해준다
  • union 타입, 튜플타입(배열의 안쪽에 아이템의 타입 선언) 사용
type User<T, U, V> = { name: T; age: U; isValid: V } | [T, U, V];

const evan: User<string, number, boolean> = ["evan", 77, true];
const lewis: User<string, number, boolean> = ["lewis", 40, false];
const leon: User<string, number, boolean> = ["leon", 33, true];

위의 코드 중에서 반복되는 코드를 재활용하여 효율적으로 관리할 수 있다

type User<T, U, V> = { name: T; age: U; isValid: V } | [T, U, V];
type U = User<string, number, boolean>; // 반복되는 부분 재활용

const heropy: U = { name: "heropy", age: 30, isValid: true };
const neo: U = { name: "neo", age: 55, isValid: true };
const amy: U = { name: "amy", age: 20, isValid: false };

const evan: U = ["evan", 77, true];
const lewis: U = ["lewis", 40, false];
const leon: U = ["leon", 33, true];

클래스에서의 제네릭

class Basket<T> {
  public items: T[];
  constructor(...rest: T[]) {
    this.items = rest;
  }
  putItem(item: T) {
    this.items.unshift(item);
  }
  takeOutItems(count: number) {
    return this.items.splice(0, count);
  }
}

const fruitsBasket = new Basket("Apple", "Banana", "Cherry");
// 명시적으로 타입변수를 선언하지 않아도 들어오는 데이터의 타입으로 타입추론 가능(string)
fruitsBasket.putItem("Orange");
const fruits = fruitsBasket.takeOutItems(2);
console.log(fruits); // ["Orange", "Apple"]
console.log(fruitsBasket.items); // ["Banana", "Cherry"]

const moneyBasket = new Basket(100, 1000, 10000);
// 명시적으로 타입변수를 선언하지 않아도 들어오는 데이터의 타입으로 타입추론 가능(number)
moneyBasket.putItem(40000);
const money = moneyBasket.takeOutItems(2);
console.log(money); // [50000, 100]
console.log(moneyBasket.items); // [1000, 10000]

제약조건

 

문자데이터만이 들어가는 구조로 사용하려면 extends라는 키워드를 사용해 제약조건을 만든다

class Basket<T extends string> {
  public items: T[];
  constructor(...rest: T[]) {
    this.items = rest;
  }
  putItem(item: T) {
    this.items.unshift(item);
  }
  takeOutItems(count: number) {
    return this.items.splice(0, count);
  }
}

다음과 같이 제약조건을 만들면 fruitsBasket.putItem("Orange") 에서 에러가 발생한다

  • 그 이유는 extends를 통해 타입변수에 제약조건을 추가하게 되면 타입스크립트가 변수의 타입을 추론을 할 때 최대한 구체적으로 타입을 추론을 한다. 따라서 들어오는 데이터의 타입이 같아도 데이터 값이 다르면 에러가 발생한다

에러를 해결하기 위해 타입변수를 명시적으로 선언해준다

const fruitsBasket = new Basket<string>("Apple", "Banana", "Cherry");
// string 타입인 모든 데이터 허용
const moneyBasket = new Basket<number>(100, 1000, 10000); 
// 위에서 string 타입으로 먼저 선언이 되었기 때문에 에러발생

제네릭의 조건부 타입

  • 유틸리티타입을 만들거나, 내장 유틸리티 타입을 해석할때 쓰인다
  • 삼항연산자를 사용한다
type MyType<T> = T extends string | number ? boolean : never;

const a: MyType<string> = true; // string => boolean
const b: MyType<number> = true; // number => boolean
const c: MyType<null> = true; // string 또는 number이 아니라 never 타입이어야 한다
type MyExclude<T, U> = T extends U ? never : T;
// string | number | boolean | null extends boolean | null ? never : string | number | boolean | null
type MyUnion = string | number | boolean | null;

const d: MyExclude<MyUnion, boolean | null> = 123;
// string | number -> T (string | number)
// boolean | null -> never

위와 같이 특정한 타입을 만들어서 필요한 경우 활용하는 경우는 유틸리티 타입이라고 한다.

그런데, 위 코드의 유틸리티 타입 부분은 이미 내장에 있다 (Exclude 타입)

const d: Exclude<MyUnion, boolean | null> = 123;

 

type IsPropertyType<T, U extends keyof T, V> = T[U] extends V ? true : false;

type Keys = keyof User;
interface User {
  name: string;
  age: number;
}

const n: IsPropertyType<User, "name", number> = true;
// User의 name 타입이 number인지 확인
// User["name"] -> string이므로 error

keyof 연산자 : 객체 형태의 타입을, 따로 속성들만 뽑아 모아 유니온 타입으로 만들어주는 연산자


제네릭의 infer

  • infer : 추론하다
  • 조건부 타입에서 타입을 추론할 때 사용한다
  • infer키워드를 통해 타입변수를 정의할건데 타입변수가 처음 만들어진 자리에서 타입변수를 추론해서 어떤 타입인지 알수 있는지 없는지 확인
type ArrayItemType<T> = T extends (infer I)[] ? I : never;
// infer I -> 타입추론을 통해 I의 타입을 추론
const numbers = [1, 2, 3];
const a: ArrayItemType<number[]> = 123;
// infer I의 타입은 number[]로 추론할 수 있다
const b: ArrayItemType<boolean> = 123; 
// error => 추론된 타입이 number[]인데 변수타입이 boolean이어서 추론된 타입과 다르기 때문에 never 타입이어야 한다
type ArrayItemType<T> = T extends (infer I)[] ? I : never;
// infer I -> 타입추론을 통해 I의 타입을 추론

const fruits = ["Apple", "Banana", "Cherry"];
const hello = () => {};

const c: ArrayItemType<typeof fruits> = "ABC";
// typeof fruits -> string[]
const d: ArrayItemType<typeof hello> = "ABC";
// typeof hello -> () => void
// hello의 타입은 함수이고 infer I는 배열이기 때문에 타입 추론을 할 수 없어 d는 never타입이어야 함

함수 인자에서 infer

type SecondArgumentType<T> = T extends (f: any, s: infer S) => any ? S : never;
// (a:string, b:number) => void extends (f:any, s: infer S) => any ? S : never
// 비교하면 infer S -> number로 추론아 되어서 true를 반환하고 S는 number타입이 됨
function hello(a: string, b: number) {}

const a: SecondArgumentType<typeof hello> = 123;
// typeof hello -> (a:string, b:number) => void

 

ReturnType 유틸리티타입

  • 함수 Type의 반환 타입으로 구성된 타입을 생성
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 함수 타입만 사용할 수 있고 함수의 인자의 타입을 반환한다
// MyReturnType은 유틸리티 타입인 ReturnType과 같은 기능을 한다
function add(x: string, y: string) {
  return x + y;
}

const a: ReturnType<typeof add> = "Hello";
// typeof add -> (x:string, y:string) => string