[TypeScript 기본 이론] #5 Generic

mechaniccoder·2020년 7월 3일
0

TypeScript 기본이론

목록 보기
5/6
post-thumbnail

Hello World!!


제네릭은 타입을 불문하고 동작하는 것을 말한다. 즉, 함수에 인수를 넘길 때 타입 인수도 넘기기 때문에 타입에 관한 어떠한 정보도 잃지 않는다. any를 쓰는 것과는 다르다. any를 쓰게 되면 만약 number타입을 넘기더라도 any타입이 반환된다는 정보만 얻을 뿐이다.

function thisIsGeneric<T>(arg: T): T {
  return arg;
}

let ouput = thisIsGeneric<string>('mystring');
console.log(ouput); // 'mystring'

타입 인수를 넘길 때는 <> 이 괄호안에 표기하면 된다.

타입 인수 추론이란??

제네릭 함수를 호출하는 두 번째 방법이다. 위의 예제와는 다르게 타입 인수를 넘겨주지 않고 컴파일러가 전달하는 인수를 보고 이를 추론한다.

function thisIsGeneric<T>(arg: T): T {
  return arg;
}

let ouput = thisIsGeneric('mystring'); // 호출할 때 타입 인수를 넘기지 않는다

타입 인수 추론은 코드를 가독성있고 간결하게 하지만 컴파일러가 타입을 추론할 수 없는 복잡한 상황에서는 타입 인수를 전달하는 것이 좋다.

제네릭 타입 변수 작업


function loggingIdentity<T>(arg: T): T {
  console.log(arg.length); // 에러, T에는 .length 가 없습니다.
  return arg;
}

위의 예제를 살펴보자. 넘긴 인수에 length 메서드를 사용했는데, 만약 타입 인수가 number라면 이는 에러를 일으킬 것이다. 따라서 애초에 에러가 발생하므로 이는 비문이다. 이를 맞게 하기 위해서는 다음과 같이 해야한다.

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length); // 에러, T에는 .length 가 없습니다.
  return arg;
}

T[]는 배열이므로 length 메서드를 가지고 있다.

제네릭 타입


제네릭 함수의 타입을 선언할 때는 비-제네릭 함수와 비슷하게 하면 된다.

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

타입 변수의 수, 사용 방식에 따라 타입 매개변수에 다른 이름을 사용할 수도 있다.

let myIdentity: <U>(arg: U) => U = identity;

객체 리터럴 방식으로 나타낼 수 있다.

let myIdentity: { <T>(arg: T): T } = identity;

그럼 제네릭 인터페이스를 작성해보자. 위의 객체 리터럴을 인터페이스로 사용했다.

interface GenericIdentityFn {
	<T>(arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn = identity;

더 나아가, 제네릭 타입 매개변수를 인터페이스의 매개변수로 사용할 수도 있다. 다음과 같이 말이다.

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

제네릭 클래스


제네릭 클래스는 제네릭 인터페이스랑 형태가 비슷하다. 클래스 이름 뒤 <>안에 타입 매개변수를 전달하고 클래스 블럭 구문에 타입 매개변수 목록들을 가진다.

class GenericNumber<T> {
	zeroValue: T,
	add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

클래스는 static, 인스턴스 두가지 타입을 가진다. 제네릭 클래스는 인스턴스의 기준에서만 제네릭이므로 제네릭을 클래스로 작업할 때 정적 프로퍼티는 클래스의 타입 매개변수를 사용할 수 없다.

제네릭 제약조건


앞에서 우리는 아래와 같은 예제를 살펴보았다.

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length); // 에러, T에는 .length 가 없습니다
  return arg;
}

만약 length 프로퍼티를 가진 타입으로 제약 조건을 걸고 싶다면 extends 키워드로 인터페이스를 사용하면 된다.

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity(3);  // 에러, number는 .length 프로퍼티가 없습니다

loggingIdentity({length: 10, value: 3}); // 성공

제네릭 제약조건에서 타입 매개변수 사용


다른 타입 매개변수를 활용해 타입 매개변수를 제한할 수도 있다.

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

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 성공
getProperty(x, "m"); // 오류:

KT의 key 타입(keyof)으로 제한했다. 이렇게 제약 조건을 걸어 멍청한 실수를 방지할 수 있다.

제네릭에서 클래스 타입 사용


팩토리를 사용할 때 생성자 함수에서 클래스 타입을 참조해야 한다. 여기서 팩토리가 뭐냐하면 객체 생성을 단순하게 하여 반복적인 객체들을 생성할 때 사용하는 객체 지향 프로그래밍에서 유래된 디자인 패턴이다.

팩토리 디자인 패턴

ES5

function Person(name) {
  this.name = name
}

Person.factory = function(name) {
  return new Person(name)
}

var ashnamuh = Person.factory('ashnamuh')

console.log(ashnamuh.name) // ashnamuh

ES6

class Animal {
  constructor() {
    this.type = 'animal'
  }

  static factory(type) {
    switch(type) {
      case 'Lion':
        return new Lion()

      case 'Cat':
        return new Cat()

      default:
        throw new Error('Invalid type!')
      }
  }
}

class Lion extends Animal {
  constructor() {
    super()
  }
}

class Cat extends Animal {
  constructor() {
    super()
  }
}

const lion = Animal.factory('Lion')
const cat = Animal.factory('Cat')

TypeScript

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

new() => A 이 부분을 잘 기억하자. 클래스 타입을 참조하는 방법이다.

References


profile
세계 최고 수준을 향해 달려가는 개발자입니다.

0개의 댓글