본문 바로가기

TypeScript/Handbook

[TypeScript] 06. Generics

원문 : https://www.typescriptlang.org/docs/handbook/generics.html



Introduction

잘 정의되고 적합할뿐만 아니라 재 사용이 가능한 컴포넌트를 만드는 일은 소프트웨어 공학의 주요한 부분이다. 어떤 데이터에건 적합한 컴포넌트는 대규모 소프트웨어 시스템을 구축하는데 매우 유연한 가능성을 제공한다.


제네릭스는 다양한 타입을 처리 할 수 있는 컴포넌트를 만들 수 있게 하여, C# 과 Java와 같은 언어에서 재사용성을 높여주는 주요한 도구로 사용된다.


Hello World of Generics

어떤 파라미터를 받아 그대로 전달해 주는 함수를 만든다고 했을 때 제네릭스를 사용하지 않는 경우 아래와 같이 작성 할 수 있다.



function identity(arg: number): number {
    return arg;
}
// 또는 any 타입을 사용 할 수 있다.
function identity(arg: any): any{
    return arg;
}


any 타입을 사용할 경우 어떤 타입의 매개변수던 다 수용 할 수 있지만 매개변수의 타입이나 리턴타입에 대한 어떠한 정보도 얻을 수 없게 된다. 

타입 파라미터를 활용 하여 이러한 문제를 해결 할 수 있다.


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

위 코드에서 함수에 타입변수 "T"를 추가 했다. 이 T를 통해 어떤 타입을 받고, 리턴 할 지 알 수 있게 된다. 

이러한 형태의 함수를 제네릭 이라고 부르며, 모든 타입에 대해 사용 할 수 있다. any를 사용하는 경우 보다 더 큰 이점을 얻을 수 있다. 


위에서 정의한 함수(identy) 를 2가지 방법으로 사용할 수 있다.


let output = identity<string>("myString");  // 타입(T)을 string으로 정의 한다. 타입 파라미터는 <> 로 감싸서 작성 한다.
let output2 = identity("myString");  // 아규먼트의 타입을 기반으로 컴파일러가 자동으로 string 타입으로 유추하여 처리 한다.

위 두번째 예제에서 보듯이 타입을 꺽인 괄호(<>)로 작성하지 않아도 컴파일러가 "myString"의 타입을 보고 유추 할 수 있다. 이는 매우 편리하고 가독성이 높아 보이지만, 컴파일러가 타입을 정확히 유추하지 못 할 수도 있기 때문에 타입을 명확히 명시할 필요가 있을 수 있다.


Working with Generic Type Variables

 제네릭스를 사용하서 함수를 선언하는 경우, 컴파일러는 제네릭스 변수가 어떤 타입이라도 될 수 있다고 간주 하고 처리한다.


function loggingIdentity<T> (arg: T): T {
    console.log(arg.length); // 에러: T 타입은 .length 라는 멤버변수가 없기 때문에 컴파일러는 에러를 발생시킨다.
    return arg;
}
function loggingIdentity2<T> (arg: T[]): T[] { //T[] 는 Array<t>로 작성 할 수 있다.
    console.log(arg.length); // arg (Array 타입)은 length 라는 멤버 변수가 있기 때문에 정상 처리 된다.
    return arg;
}


Generic Types

 함수 자체의 type과 제네릭 인터페이스를 생성하는 방법에대해 알아보자.

 제네릭 함수는 타입 파라미터를 명시하는것을 제외 하면 일반함수와 똑같다. 


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

타입 파라미터의 수와 타입 파라미터의 사용방법이 일치하는 한 제네릭 타입 매개변수에 다른 문자를 사용 할 수 있다.


function loggingIdentity<T> (arg: T): T {
    return arg;
}
//"T"로 정의 되어 있지만 어차피 정확한 타입이 정해진게 아니기 때문에 "U"로 사용해도 된다.
let myIdentity: <U>(arg:U) => U = identity;

제네릭 타입을 객체 리터럴 타입으로 작성 할 수도 있다.


function loggingIdentity<T> (arg: T): T {
    return arg;
}
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;

제네릭 함수를 이용하는 대신, 인터페이스에 선언된 제네릭 타입을 이용한 비 제네릭 함수를 선언하는 것으로 변경했다. GenericIdentityFn 인터페이스를 이용할 때 알맞은 타입 파라미터를 지정해야 하며, 이를 통해 내부 함수에서 사용되는 변수 타입이 고정(locking) 된다.

제네릭 인터페이스 뿐만 아니라 제네릭 클래스도 생성 할 수 있다.


Generic Classes

 제네릭 클래스는 제네릭 인터페이스와 똑같이 클래스 명에 타입변수를 꺽은 괄호(<>)로 감싸서 작성한다.


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; };

위 예제는 GenericNumber 클래스의 리터럴 사용법 이다. 위예제에서는 타입변수를 number로 지정했기때문에 클래스에 선언된 T는 number 라고 보면된다. number 대신 string을 사용 할 수도 있다.


let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

인터페이스와 마찬가지로 클래스 선언 시 타입을 선언하면 클래스의 제네릭 값들을 모두 선언된 타입으로 사용 할 수 있다.


클래스에는 static side와 instance side 가 있는데 제네릭 클래스는 instance side에서 구현된다. 따라서 static 멤버 에서는 타입 매개변수를 사용 할 수 없다.


Generic Constraints

 제네릭을 사용하다보면 간혹 사용하고자 하는 타입에 대해 어느정도 제약을 설정 하거나, 타입이 가지고 있는 어떠한 속성이나 함수를 사용하고 싶은 경우들이 있다.

 예를 들어 어떤 변수를 받아서 (타입이 뭐가 될지는 모름) 그 변수의 길이를 log로 출력하는 함수를 만든다고 하자.


function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

위 예제는 에러가 발생한다. T 에 어떤 타입이 올지 모르고, 그 타입이 length라는 속성을 가지고 있을지, 없을지 모르기 때문이다.

이런 경우 아래와 같이 arg를 length 속성을 가지고 있는 타입으로 제한을 걸어 해결 할 수 있다.


//먼저 length(number 타입) 속성을 갖는 interface를 정의한다.
interface Lengthwise {
    length: number;
}

//타입변수 T는 length 속성을 갖는 Lengthwise를 상속한(구현한) 타입으로 제한한다.
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}


Using Type Parameters in Generic Constraints

 다른 타입 파라미터에 제약 조건으로 걸리는 타입 파라미터도 정의 할 수 있다. 예를 들어 어떤 오브젝트가 어떠한 값 들을 가지고 있고 그 오브젝트가 가지고 있는 값들만 사용하고 싶은 경우 아래와 같이 어떤 오브젝트 (T 타입)와 그에 따르는 속성 값 (K extends key of T 타입)으로 제약 조건을 걸 수 있다.


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"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.


Using Class Types in Generics

 팩토리 함수를 제네릭스를 이용하여 작성 하면 생성되는 클래스 타입을 지정 할 수 있다.


function create<T>(c: {new(): T; }): T {
    return new c();
}

위 방법을 이용하면 아래와 같은 코딩이 가능해 진다.


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();
}

createInstance(Lion).keeper.nametag;  // 생성되는 타입이 Lion 이므로 keeper의 타입은 ZooKeeper이고 nametag 속성을 갖는다.
createInstance(Bee).keeper.hasMask;   // 생성되는 타입이 Bee 이므로 keeper의 타입은 BeeKeeper이고 hasMask 속성을 갖는다.


반응형

'TypeScript > Handbook' 카테고리의 다른 글

[TypeScript] 05. Functions  (0) 2018.01.31
[TypeScript] 04. Classes  (0) 2017.07.07
[TypeScript] 03. Interfaces  (0) 2017.05.04
[TypeScript] 02. 변수 선언  (0) 2017.04.25
[TypeScript] 01. Basic Types  (0) 2017.04.24