TypeScript: Generics Ep.2

Generics Class


class Person<T> {
  private _name: T;

  constructor(name: T) {
    this._name = name;
  }
}

new Person("mark"); // string
// new Person<string>(29); // error
  • T의 유효범위는 constructor나 private에서 발생하는 것이 아니라, 클래스 전체에서 타입의 유효범위가 발생한다.
  • 따라서 generic으로 T를 선언해야 한다면 class 이름 뒤에 T를 지정한다.
  • Person의 T가 string으로 들어간다.
  • 만약 generic을 미리 세팅한다면(string으로 지정), 인자 부분에는 string만 들어와야 한다. number가 들어오면 error
class Person<T, K> {
  private _name: T;
  private _age: K;

  constructor(name: T, age: K) {
    this._name = name;
    this._age = age;
  }
}

new Person("mark", 29); // string
new Person<string, number>("Ban","Kan"); // error
  • generic을 하나 더 추가하기 위해서 그대로 그냥 추가해 주면 된다..!
  • 인자를 두개 받아야 하기에 하나만 받으면 error가 발생한다.
  • generic을 통해 타입을 지정해주었을 때, 지정한 타입이 아닌 경우 error가 발생한다.

Generics with extends


우리가 흔히 생각하는 상속의 개념과는 약간 다른 generic extends

해당 개념을 사용하는 이유는 다른 사람이 나의 코드를 사용할 때, 예상치 못한 error을 막기 위함이다.
따라서, 유용하게 사용하면 좋은 키워드이다.

class PersonExtends<T extends string | number> {
  private _name: T;

  constructor(name: T) {
    this._name = name;
  }
}

new PersonExtends("Mark");
new PersonExtends(20);
new PersonExtends(true); // error
  • class 이름 뒤에 T에 extends 키워드를 이용해서 타입을 추가로 넣어줄 수 있다.
  • extends 키워드 뒤에 union 타입을 추가해주게 된다.
    이를 해석하면, “T는 string과 number만 타입으로 갖는 것이 가능하다.” 는 말이다.

keyof & type lookup system


(컴파일 타임에서) 타입을 적절히 찾아내고 활용하는 시스템 만들기

interface IPerson {
  name: string;
  age: number;
}

const IPer: IPerson = {
  name: "Mark",
  age: 19,
}

function getProp(obj: IPerson, key: "name" | "age"): string | number {
  return obj[key]
}

function setProp(obj: IPerson, key: "name" | "age", value: string | number): void {
  obj[key] = value;
}
  • name일때, IPerson에 name을 넣은게 나와야 하고, age일때 IPerson에 age를 넣은게 나와야 하는데 union 타입이 되어서 작성하기 어렵고 타입에 문제가 발생한다.

keyof

타입 앞에 붙여서 새로운 타입을 만들어 낸다.

interface IPerson {
  name: string;
  age: number;
}

const IPer: IPerson = {
  name: "Mark",
  age: 19,
}

// type Keys = keyof IPerson;
// const keys: Keys = "name";
// const keys: Keys = "age";

// IPerson[keyof IPerson] 
// => IPerson[”name” | “age”] 
// => IPerson[”name”] | IPerson[”age”]
// => string | number
function getProp(obj: IPerson, key: keyof IPerson): IPerson[keyof IPerson] {
  return obj[key]
}

function setProp(obj: IPerson, key: keyof IPerson, value: string | number): void {
  obj[key] = value;
}
  • keys는 age, name을 가지고 있는 형태이다.
  • 어떤 개체에 keyof를 붙이면 결과물이 type으로 나오는데, key의 이름으로 된 문자열 (name, age)의 union 타입으로 만들어진다.
  • 따라서 string | number을 적어주었던 인수부분에 keyof IPerson을 적을 수 있다.
  • return되는 결과물에는 IPerson[keyof IPerson]으로 작성해 줄 수 있다.
  • 단, 해당 결과물은 다음과 같이 나온다.
    IPerson[keyof IPerson] ⇒ IPerson[”name” | “age”] ⇒ IPerson[”name”] | IPerson[”age”]
    결과적으로 string과 number의 union타입이 된다.
  • getProp을 넣었을 때, name을 넣으면 string이 결과물로 나오게끔 하지 못했다.

Generic

IPerson과 keyof IPerson을 이용해서 관계를 특정한 타입으로 지정을 해줘야 한다. 이를 위해 generic을 사용한다.

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

// getProp의 결과물
getProp(per ,'name'); // string
getProp(per, 'age') // numner

function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value;
}

setProp(per, "name", 39) // name과 맞지 않는 타입이기에 error
setProp(per, "name", "Anna") // 맞는 타입이기에 값 변경 완료. value는 string이 됨.
  • IPerson자리에 T를 대신 사용한다.
  • K를 만들어서 extends를 사용하여 제한을 걸어준다.
    제한은 name과 age만 넣을 수 있도록 한다.
    <T, K extends keyof T>
    K는 keyof T에 의해서 제한된 형태(name 혹은 age)가 된다.
  • 따라서 keyof T 자리에 K를 대신 넣어 사용한다.
    K는 들어오는 것에 따라 name 혹은 age로 딱! 지정된다.
  • 해당 방법으로 인해 런타임을 하기 전에 문자를 잘 못 넣게 되면 error로 알려준다.
  • setProp은 value자리에 name혹은 age와 맞지 않은 타입이 들어가게 되면 error가 발생하고 적절한 타입을 넣어주면 값이 변경된다.
    그리고 value의 타입이 해당 name 혹은 age의 타입으로 변경이 된다.

Comments