TypeScript Generic 완

Generics

Type Script Generic 파트 번역본

Index

  1. Introduction

  2. Hello World of Generics

  3. Working with Generic Type Variables

  4. Generic Types

  5. Generic Classes

  6. Generic Contraints

Introduction

소프트웨어 엔지니어링의 중요한 부분은 잘 정의되었을 뿐만아니라 일관된 된 API들을 가진 컴포넌트를 만드는것 뿐만 아니라, 그 컴포넌트들이 재사용 가능해야 한다는 것이다. 내일의 데이터 뿐만아니라 오늘의 데이터에서 작동할 수 있는 컴포넌트는 대규모 소프트웨어를 만들기 위해 가장 유연한 능력을 줄 것이다. C#이나 Java 와 같은 언어에서와 같이, 재사용가능한 컴포넌트를 만들기위해 툴박스에서 중요한 툴중 하나는 바로, 단순히 하나의 타입에서만 작동하는 것이 아니라 여러 타입에서 작동하는 컴포넌트를 만둘스 있게 해주는 Generics이다. Generics는 사용자로 하여금 컴포넌트를 소비하고 그들의 타입을 사용할 수 있게 허락한다.

Hello World of Generics

시작하기 위해 Identity function을 활용해 hello world를 제네릭으로 만들어보자. (Identity function이란 인자값으로 넘겨진 것을 고스런히 반환하는 함수를 말한다. echo 커멘드와 비슷하게 작동한다고 생각하면 된다.)

제네릭 없이는 우리는 직접 인자값에 타입을 지정해 주거나

function identity(arg: number): number {
    return arg;
}

또는 우리는 Identity function을 any타입으로 구현할 수 있다.

function identity(arg: any): any {
    return arg;
}

any타입역시 모든 타입을 받아들일 수 있기 때문에 Generic 하다고 말할 수는 있지만, 함수가 반환되었을때, 반환된 값이 어떤 타입인지를 놓치게 된다. 숫자를 인자값으로 넘겼을 때, 오직 any 타입이 반환된다는 것만을 알 수 있다.

대신, 어떤 값이 반환이 될 것인지를 인자값을 통해 명시할 방법이 필요하다. 여기서 특별한 변수를 사용할 것인데, 이 값대신 타입에서 작동한다.

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

타입변수 T를 identity함수에 추가했다. 이 T는 사용자가 인자값으로 제공하는 타입을 캡쳐해 우리가 그 정보를 나중에 사용할 수 있게 해준다. 여기서 T를 다시 리턴타입에 사용하였다. 이를통해 우리는 인자값과 리턴값이 동일하다는 사실을 알 수 있다. 이것은 어떤 타입이 들어가는지와 나가는 타입의 정보를 다룰 수 있게 해준다. 이런 identity function은 모든 타입과 작동하기때문에 generic하다고 말한다. any를 사용했던 첫번째 identity 함수와는 달리 Generic을 쓴 함수는 어떤 정보도 놓치지 않아 정교해진다.

한번 identity 함수를 쓰면, 우리는 두가지 방식으로 이것을 호출할 수 있다. 첫번째 방법은 타입 인자값과 함께 모든 인자값들을 함수에 넘기는 것이다.

let output = identity<string>("myString"); // type of output will be 'string'

여기서 명시적으로 T를 string을 함수호출의 하나의 인자값으로써 세팅한다. (() 표현 대신에 <>를 사용하여 표기한다.) 2번째 방법이 가장 일반적인 방법일 것이다. 여기서, 타입 인자값 암시를 사용한다. 즉, 우리는 컴파일러가 자동으로 인자값의 타입에 따라 T를 해석하게 한다.

let output = identity("string");

여기서 명시적으로 <>에 타입을 지정해줄 필요가 없다는데에 주목할 필요가 잇다. 컴파일러는 인자값으로 넘어간 값이 string이라는 것을 보고 T를 string으로 자동으로 세팅해준다. type inference를 사용하는 것이 코드를 읽기쉽고 짧게 유지보수하는데 유리할 수 있지만, 더 복잡한 상황에서 컴파일러가 타입을 읽지 못할경우 명시적으로 타입을 입력해야 할 수도 있다.

Working with Generic Type Variables

제네릭을 사용하기 시작할 때, 위의 예제에서 identity함수와 같은 generic을 만들려고 할 때, 컴파일러가 강제적으로 함수의 바디에서 generic으로 지정된 인자값의 타입의 사용하도록 강제한다는 점에 주목해야 한다. 즉, 당신은 이런 인자값들을 마치 any나 all 타입인 것처럼 다룰 수 있다.

이전에 사용했던 identity 함수를 보자.

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

만약 console.log의 인자값의 길이를 출력할려고 하면 어떻게 될 것인가?

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

위의코드를 실행하려고 했을때 컴파일러는 에럴를 발생시키는데, .length를 어디에서도 찾을 수 없기 때문이다.
타입변수는 모든 값을 포함해서, 누군가는 .length 속성이 없는 number타입을 인자값으로 넘길 수 있다는 점을 기억해야 한다.

&Newline;

이 함수에서 T를 직접쓰는 것이 아니라 T의 배열을 인자값의 타입으로 쓴다고 가정해보자.

인자값으로 배열을 넘겼기 때문에 .length가 사용할 수 있어야 한다.
다른 타입의 배열을 만들듯 우리는 generic을 구현할 수 있다.

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

위의 loggingIdentity함수를 통해 이 함수가 type variables로 T를 받고 인자값으로 T의 배열을 받으며 반환값으로 T의 배열을 받는다는 것을 알 수 있다. 만약 인자값으로 수의 배열을 넘긴다면 반환값으로 마찬가지로 숫자의 배열을 받을 것이다. 이런 작동은 우리에게 훌륭한 유연성을 제공하면서 우리가 다루고 있는 타입들의 부분으로써 generic type variable을 사용 할 수 있게 허락해 준다. 아마 다른 언어에서 비슷한 스타일을 봤을 수도 있다. 다음 섹션에서는 우리는 어떻게 고유의 generic 타입을(e.g. Array<T>) 만들어 낼 수 있는지 알아볼 것이다.

Generic Types

지난 섹션에서 우리는 identity 함수를 type variable을 이용하여 모든 타입과 작동하게 만들었다. 이번 섹션에서는 generic interfaces를 어떻게 만드는지와 함수 그 자체의 타입을 알아볼 것이다.

generic 함수들의 타입은 generic이 아닌 함수들의 선언과 비슷하다.

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

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

Type variables의 수와 어떻게 Type variables들의 순서가 일치하는 한 generic 타입 인수에 다른 이름을 사용할 수 있다.

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

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

또한 Object literal 유형의 call signature(interface part 참조)로써 Generic 유형을 쓸 수 있다.

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

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

위의 방식은 첫번째 generic iface를 만드는 것으로 이끌어 준다. 이전 예제에서 Object literal 부분을 가져와 iface로 옮겨보자.

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

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

let myIdentity: GenricIdentityFn = identity;

비슷한 예제에서, 아마 generic 인자를 interface 전체의 인자값으로 옮기고 싶을지도 모른다. 이것은 우리로하여금 어떤 타입이 generic한지 알려줄것 이다.( 예를 들자면 Dictionary<string>Dictionary를 쓰는 것) 이것은 모든 인터페이스의 다른 멤버들에게 타입 인자들을 보이게 만들어 준다.

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

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

let myIdentity: GenericIdentityFn<number> = identity;

예제를 아주 약간 바꿨다는 점에 주목해야한다. Generic 함수를 구현하는 대신에 call signature를 generic type으로 가지는 non-generic 함수를 가지게 되었다. GenericIdentityFn를 사용할 때, 일치하는 type 인자값(여기선 number)를 구체적으로 명시하며, 효과적으로 call signature가 어떤 타입을 사용할지를
고정시킬 필요가 있다.

직접 타입인자를 사용하는 것과 interface에 타입인자를 사용하는 것을 언제 해야하는지 이해하는 것은 어떤 타입의 측면들이 generic한지 구현하는데 유용할 것이다.

Generic iface 이외에도 generic class도 만들 수 있다. 그렇지만 generic enum이나 generic namespace를 만들 수는 없다는 점을 주의하자.

Generic Classes

Generic class는 Generic interface와 비슷한 형태를 하고 있다. Generic class는 generic type 인자값 리스트를 브라켓(<>)에 가진다.

class Generic<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 class의 상당히 있는 그대로의 사용이지만, 오직 number 타입만으로 사용하는 것을 규제하는 것이 없다는 점에 주목해야 합니다. 대신 string을 사용할 수도 있고 복잡한 객체를 사용할 수도 있을 것입니다.

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

alert(stringNumeric.add(stringNumeric.zeroValue, 'test'));

iface와 같이 클래스 그자체에게 타입 인수를 넘기는 것은, 모든 속성들이 클래스의 type 인자로 넘어온 타입과 같은 타입을 사용한다고 확신시켜 준다. 클래스 섹션에서 얘기했듯이, 클래스는 두가지 측면을 가지는데, 하나는 정적, 그리고 나머지 하나는 객체 부분이다. Generic class는 오직 객체 에서만 generic을 사용가능하다. 그렇기 때문에 class 작업을 할때 정적 멤버들은 generic을 사용이 안된다는 점을 유념하자.

Generic Constraints

이전 예제들을 기억하고 있다면, 특정한 타입들과 작동하는(내부 메서드를 알고 있는) generic 함수를 만들고 싶을지도 모른다. loggingIdentity예제에서 .length 속성에 접근하고 싶었지만 모든 타입이 .length 속성을 가지고 있는 것이 아니라 컴파일러는 우리에게 .length가 타입에 있을 거라는 가정을 하는 것을 호용하지 않고 에러메세지를 보내준다.

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

any와 모든타입과 작동하는 데신에 우리는 .length 속성이 있는 모든 타입과 any와 작동하는 함수로 제한하고 싶을지도 모른다. 이 타입이 .length 가지는 한, 허용할 것이지만, 적어도 .length멤버를 가지고 있는 것이 요구된다. 그렇게 하기 위해서, 반드시 T가 무엇이 되어야 하는지 제한을 걸어주어야 한다.

제한을 걸기 위해, 제한을 구현시키는 iface를 만들 것이다. 여기서 .length 속성이 있는 iface를 만들 것이고 그다음 extends키워들 통해 제한을 나타낸다.

interface Lengthwise {
    length: number;
}

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

이제 제한을 걸었기 때문에 아무 타입이나 하고 작동하지 않는다.

loggingIdentity(3); // Error, number doesn't have a .length property

대신에 우리는 모든 type 요구조건이 충족된 값을 인자값으로 넘겨야 한다.

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

Using Type parameters in Generic Constraints

다른 type 인수에의해 제한되는 type 인수를 선언한다. 예를 들자면, 여기서, 주어진 이름으로 객체의 속성을 얻고싶다고 하자. 실수로라도 객체에 존재하지 않는 다른 속성을 갖지 않다록 하고싶다, 그래서 2개의 타입에 제한을 둘 것이다.

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

Using Class Types in Generics

Generics를 이용해서 타입스크립트에서 factory 를 만들 때, 클래스 타입들을 그들의 생성자 함수에 의해 참조하는 것은 필수적입니다. 예를 들자면

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; //typecheck!
createInstance(Bee).keeper.hasMask; //typecheck!