studye / typescript

타입스크립트는 자바스크립트랑 다른 언어인가요?
7 stars 0 forks source link

[Chapter 4] Generics #15

Open casiru opened 7 years ago

casiru commented 7 years ago

Introduction

Generic은 코드의 일관성을 유지하면서 재사용성을 높이기 위해 사용된다.

예를들어, 아래의 코드는 type을 number를 사용하므로 잘 정의되고, 일관 되었지만 다른 type에 대해서는 사용할 수 없다.

function do(arg: number): number {
  ...
}

그렇다고 아래와 같이 정의하기엔 정의를 보고 어떤 기대도 할 수가 없다.

function do(arg: any): any {  // 그래서, 뭐가 return 된다는 거지?
  ...
}

특정 component를 여러 type에 대해 재사용하고 싶지만 최소한, 사용하는 입장에서 무언가 기대할 수 있는 정의가 필요 할 때. Generic이 하나의 방법이 된다.

Syntax

hello world

두 인자를 console에 보여 주는 함수를 정의한다.

function printArgs<T>(arg1: T, arg2: T): void {
    console.log (`${arg1} ${arg2}`)
}

해당 함수는 아래와 같이 사용 가능하다.

printArgs('hello', 'world') // hello world
printArgs(1, 2) // 1 2
printArgs<string>('hello', 'world') // hello world

사용시에 type T를 정의하는 선택사항이다.

이렇게 하면 error 가 발생한다.

//  error TS2453: The type argument for type parameter 'T' cannot be inferred from the usage
printArgs(1, 'world') 

//error TS2345: Argument of type '"hello"' is not assignable to parameter of type 'number'.
printArgs<number>('hello', 'world')

Generic Types

Generic을 사용하여 type을 정의 할 수도 있다.

Generic을 사용한 함수를 type으로 나타낼 때 단순히 type을 Function으로 줘도 문제가 되지 않지만

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

그러면 아래와 같은 잘못 된 사용에 대해 경고를 줄 수가 없다.

myIdentity(1, 2, 3) // compile error 발생하지 않음

그래서 조금 더 명확하게 함수를 정의 하도록, 여럿 type 정의 방법들에 대해 알아 본다.

함수 정의 문법 사용

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

당연한 말이지만 T 대신 U와 같은 다른 문자열도 사용 가능 하다.

object literal 사용

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

interface를 정의해 사용

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

잠깐!!

함수 type을 정의 할 때 object literal과 interface를 사용해 정의하는게 신기해서 찾아 봤습니다. interface는 주로 object를 정의 할 때 사용했는데, 함수를 정의 할 수 있네요.

interfaceFunction Types

가령 아래와 같이 사용할 수 있습니다.

function dum(a) {
    return a
}

var numberOrString: { (a: string): string, (a: number): number } = dum

numberOrString(1)
numberOrString('a')
numberOrString(true) // error TS2345: Argument of type 'true' is not assignable to parameter of type 'number'.

Reusing definition by Generic

위에서 정의한 함수를 사용해 interface를 재사용하는 예를 보자

interface GenericIdentityFn<T> {
    (arg: T): T;
}
let numberIdentity: GenericIdentityFn<number> = identity;
let stringIdentity: GenericIdentityFn<string> = identity;

numberIdentity(1)
stringIdentity('1')
stringIdentity(1) // error TS2345: Argument of type '1' is not assignable to parameter of type 'string'.

만약 Generic을 사용하지 않는 다면 아래와 같은 코드를 사용해야 할 것이다.

let numberIdentity: { (arg: number): number } = identity;
let stringIdentity: { (arg: string): string } = identity;

비록 이 예제는 짧은 정의를 다루지만, 정의 자체가 복잡해 진다면 Generic은 좋은 방법이 될 것으로 보인다.

Generic Class

Generic을 사용해 정의한 class의 예이다.

class Foo<T> {
    private list: T[]

    constructor() {
        this.list = []
    }

    append(item: T) {
        this.list.push(item)
    }
}

함수에서의 Generic은 함수 내 life cycle인 input / output 의 type 일관성을 보장하는데, class에서의 사용은 해당 class로 만든 instance의 life cycle내에서 type 일관성을 보장한다. 아니, 해야 할 것 같다! 그럴려고 사용하는 거니까

가령 아래의 코드는 error를 반환하기를 기대 한다.

let foo = new Foo()
foo.append(1)
foo.append(2)
foo.append('a') // but, it works well..;;

하지만 위 코드는 error가 발생하지 않는다. 왜냐면 위와 같은 사용은 append method 내에서의 type 일관성만 보장 할 뿐이다.

instance 의 life cycle 내에서 type 일관성을 주려면 아래와 같이 생성 해야 한다.

let foo = new Foo<number>()
foo.append(1)
foo.append(2)
foo.append('a') //  error TS2345: Argument of type '"a"' is not assignable to parameter of type 'number'.

Generic Constraints

때로는 Generic type으로 모든 type을 받고 싶진 않고, 일부 type중 하나만 받고 싶을 때가 있다. 아래의 예를 보자

class Foo<T> {
    private v: T

    constructor(arg: T) {
        this.v = arg
    }

    getLength() {
        return this.v.length
    }
}

var foo = new Foo(1)  // no compile error
foo.getLength() // undefined

위 예에서 생성자의 인자로 number는 어울리지 않는다. 어떤 type이던 length가 있었으면 좋겟다.

이를 위해 아래와 같이 정의 할 수 있다.

interface Lengthwise {
    length: number
}

class Foo<T extends Lengthwise> {

혹은,

type StringOrArray = string | Array<any>

class Foo<T extends StringOrArray> {

이제 new Foo(1) 은 compile error를 발생 시킨다.

Creating factories within generics

Generic을 사용해 여러 type의 instance를 만드는 factory 를 만들어 보자

class Tree {
}
class Forest<T> {
  create(): T {
    return new T()
  }
}

var forest = new Forest<Tree>()
forest.create();

이렇게 사용하면 나무를 만들어 주면 좋겠다. 하지만 이 예제에는 Generic에 대해 크게 오해하는 점이 하나 숨어(?) 있다.

바로 아래 error가 말하는 점이다.

 'T' only refers to a type, but is being used as a value here.

Generic은 type으로 사용되지, value로 사용되지 않는다. 그리고 javascript에서 Class는 그 자체로도 사용할 수 있는 value이다.

그래서 결국 아래와 같이 factory 함수의 인자로 Class를 넣어 주어야 한다.

class Forest<T> {
  create(c): T {
    return new c()
  }
}

var forest = new Forest<Tree>()
forest.create(Tree)

하지만 factory를 이렇게 사용하면 조금 아쉽다. 왜냐면 아래와 같은 코드도 compile error가 발생하지 않기 때문이다.

forest.create(1); // no compile error. but run time error occur

이에 대한 error를 주기 위해 factory를 아래와 같이 수정 할 수 있다.

class Forest<T> {
  create(c: { new(): T }): T {
    return new c()
  }
}

해당 정의는 constructor 함수를 가질 수 있음을 의미한다. (이건 그냥 외우기..^^;;)