ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] TypeScript 기초 - 5
    programing/Language 2020. 1. 31. 15:45

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)

     

    2020/01/24 - [programing/JavaScript] - [TS] TypeScript 기초 - 1

    2020/01/25 - [programing/JavaScript] - [TS] TypeScript 기초 - 2

    2020/01/26 - [programing/JavaScript] - [TS] TypeScript 기초 - 3

    2020/01/28 - [programing/JavaScript] - [TS] TypeScript 기초 - 4

    2020/01/31 - [programing/JavaScript] - [TS] TypeScript 기초 - 5

    2020/02/01 - [programing/JavaScript] - [TS] TypeScript 기초 - 6


    이번 포스트에서는 타입스크립트 핸드북을 간단하게 정리한 글입니다.

    한글로 번역된 좋은 페이지도 있으므로, 글 하단에 링크 남겨두겠습니다.

     

     

    Type Inference

    TS 및 JS는 변수의 자료형에 대한 명시가 없다면, 자료형을 추론합니다.

    다음과 같은 경우에 자료형을 추론합니다.

    • 변수 혹은 멤버를 초기화할 때
    • 기본 파라미터를 설정할 때
    • 함수의 반환값을 결정할 때

     

    Best common type

    다양한 표현식에 의해 자료형이 추론될 때, 해당 표현식의 자료형은 가장 일반적인 자료형(best common type)을 계산하는데 사용됩니다.

     

    let x = [0, 1, null];

    위 코드에서 x의 자료형을 추론하기 위해, 반드시 모든 요소들의 자료형을 고려해야 합니다.

    가장 일반적인 자료형 알고리즘은 각 후보 자료형을 구한 뒤, 다른 모든 후보와 비교가능한 후보를 고릅니다.

    가장 일반적은 자료형은 주어진 후보 자료형들 중에서 선택되어야 하며, 각 후보 자료형들이 공유하는 일반적인 구조는 있을 수 있지만, 다른 모든 후보 자료형들의 상위집합인 자료형이 없는 경우가 있습니다.

    class Animal { }
    class Rhino extends Animal { }
    class Elephant extends Animal { }
    class Snake extends Animal { }
    
    // type : (Rhino | Elephant | Snake)[]
    let zoo = [new Rhino(), new Elephant(), new Snake()];

    변수 zoo의 자료형은 Animal[]로 추론되는 것이 이상적이지만, zoo내부에 Animal형인 요소가 없기 때문에 그렇게 추론하지는 않습니다. 대신, TS 컴파일러는 (Rhino | Elephant | Snake)[]로 추론합니다.

    우리가 의도한 자료형이 되기를 바란다면, 자료형을 Animal []로 명시해야 합니다.

     

    일반적으로, 가장 일반적인 자료형을 추론하기 위한 후보들에는 다음과 같은 것들이 있습니다.

    • 함수 호출 인자
    • 할당문의 우변
    • 타입 어설션(type assertion)
    • 객체의 멤버
    • 배열 리터럴
    • 반환문

     

    Contextual Typing

    TS에서는 가장 일반적인 자료형과 다른 방식으로 추론할 때도 있습니다. 이것을 문맥적 타이핑(contextual typing라고 합니다.

    문맥적 타이핑은 표현식의 자료형이 자신의 위치에서 암시될 때 일어납니다.

     

    window.onmousedown = function(mouseEvent) {
        console.log(mouseEvent.button);   //<- OK
        console.log(mouseEvent.kangaroo); //<- Error!
    };

    위 코드에서 TS 자료형 확인자(type checker)는 우변값의 함수표현식의 자료형을 추론하기 위해 onmousedown의 자료형을 사용합니다. 그러면 mouseEvent의 자료형을 추론하며, button속성을 가지고 있음을 알 수 있습니다. 그러나 추론된 mouseEventkangaroo속성을 가지고 있지 않으므로 에러가 발생합니다.

     

    window.onscroll = function(uiEvent) {
        console.log(uiEvent.button); //<- Error!
    }

    TS는 똑똑해서, onscroll속성에 할당되는 함수의 인자는 button속성을 가지고 있지 않음을 알고 있습니다.

     

    const handler = function(uiEvent) {
        console.log(uiEvent.button); //<- OK
    }

    그러나 위 코드처럼 문맥적으로 타이핑을 할 수 없는 경우에는 함수의 인자의 자료형이 any로 추론되어, 에러가 발생하지 않습니다. 조심해야겠죠?

     

    가장 일반적인 자료형에도 문맥적 타이핑를 사용합니다. 

    class Animal { }
    class Rhino extends Animal { }
    class Elephant extends Animal { }
    class Snake extends Animal { }
    
    function createZoo(): Animal[] {
        return [new Rhino(), new Elephant(), new Snake()];
    }
    // type : Animal[]
    let zoo = createZoo();

    위 코드에서 반환하는 배열의 자료형을 추론하기 위한 후보들에는 반환값의 자료형인 Animal도 포함됩니다. 따라서 후보 자료형은 Rhino, Elephant, Snake, Animal로 총 4개이며, 여기서 상위집합인 Animal이 가장 일반적인 자료형이 되고, 반환값은 배열이므로 결국 zoo의 자료형은 Animal[]이 됩니다.

     

     

    Type Compatibility

    TS의 자료형 비교는 구조적 서브타이핑(structural subtyping)를 기반으로 합니다. 구조적 타이핑은 해당 값의 멤버만 가지고 자료형을 관계짓는 방법입니다. 이것은 명사적 타이핑(nominal typeing)과 대조됩니다.

     

    interface Named {
        name: string;
    }
    
    class Person {
        name: string;
    }
    
    let p: Named;
    // OK, because of structural typing
    p = new Person();

    자바와 같은 경우, Person은 Named를 implements하지 않았기 때문에 에러가 나는 코드입니다만, TS에서는 허용이 됩니다.

    왜 허용을 했을까요?

    왜냐하면 JS에서는 익명 객체를 광범위하게 사용하기 때문에, 명사적 타이핑으로 관계를 판단하는 것보다 구조적 서브타이핑으로 관계를 판단하는게 더 자연스럽기 때문입니다.

     

     

    Iterator, Generator

    제 블로그 글을 참고해주세요.

    https://kjwsx23.tistory.com/282

    https://kjwsx23.tistory.com/283

     

     

    Decorator

    데코레이터는 클래스 혹은 클래스 멤버에게 어노테이션(annotation)을 추가할 수 있고, 메타 프로그래밍 문법(meta-programming syntax)을 지원합니다.

    TS에서도 실험적인 기능이기 때문에, playground에서는 테스트가 불가능합니다.

    데코레이터는 클래스 선언문, 메소드, 접근자, 속성, 파라미터에 붙을 수 있습니다.

    데코레이터는 @expression의 형태로 사용하며, expression은 반드시 "런타임에 데코레이팅된 선언에 대한 정보와 함께 호출될 함수"로 평가되어야 합니다.

    setting

    {
        "compilerOptions": {
            "target": "ES5",
            "experimentalDecorators": true
        }
    }

     

    Decorator Composition

    데코레이터의 합성은 다음과 같이 할 수 있습니다.

    • single line
      • @f @g x
    • multi line
      • @f\n@g\nx

    함수 합성과 유사하게, 위 코드는 $(f\circ g)(x)$, $g(f(x))$입니다. 즉, 표현(evaluate)은 top-down, 실행(call)은 bottom-up입니다.

     

    Class Decorators

    클래스 데코레이터는 class 키워드 직전에 위치합니다. 클래스의 생성자에 적용됩니다. 관찰, 수정, 클래스 정의를 대체하는데 사용됩니다.

    그러나 선언 파일(declaration file)이나 포위된 클래스(ambient context)에는 사용할 수 없습니다.

    클래스 데코레이터 표현식은 "데코레이팅된 클래스의 생성자를 유일한 인자로 받는 함수"로서 런타임에 호출됩니다.

     

    function sealed(constructor: Function) {
        Object.seal(constructor);
        Object.seal(constructor.prototype);
    }
    
    @sealed
    class Greeter {
        greeting: string;
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            return "Hello, " + this.greeting;
        }
    }

    위 코드에서 Greeter 클래스를 이용한 인스턴스가 만들어질 때, sealed 데코레이터가 실행이 되어 생성자와 생성자의 프로토타입을 봉인합니다.

     

    // extends를 이용하여, T는 생성자 함수만 받도록 제한을 걸 수 있습니다.
    function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
        return class extends constructor {
            newProperty = "new property";
            hello = "override";
        }
    }
    
    @classDecorator
    class Greeter {
        property = "property";
        hello: string;
        constructor(m: string) {
            this.hello = m;
        }
    }
    
    console.log(new Greeter("world"));

    위 코드처럼, 생성자를 오버라이딩 할 수도 있습니다.

     

    Method Decorators

    메소드 데코레이터는 메소드 선언 바로 직전에 위치합니다. 메소드의 속성 기술자(property descriptor)에 적용됩니다. 관찰, 수정, 메소드 정의를 대체하는데 사용됩니다.

    그러나 선언 파일이나 오버로드, 포위된 컨텍스트에는 사용할 수 없습니다.

    메소드 데코레이터 표현식은 "다음 세가지 인자를 받는 함수"로서 런타임에 호출됩니다. 

    1. 정적 멤버를 위한 클래스의 생성자 함수 혹은 인스턴스 멤버를 위한 클래스의 프로토타입 (뭔소리여)
    2. 멤버의 이름
    3. 멤버의 속성 기술자 (target이 ES5이하라면 undefined입니다.)

    만약 메소드 데코레이터가 값을 반환한다면, 그 값은 속성 기술자로서 사용됩니다. (target이 ES5이하라면 반환값은 무시됩니다.)

     

    function enumerable(value: boolean) {
        return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
            descriptor.enumerable = value;
            console.log(target);
            console.log(propertyKey);
            console.log(descriptor);
        };
    }
    
    class Greeter {
        greeting: string;
    
        constructor(message: string) {
            this.greeting = message;
        }
    
        @enumerable(false)
        greet() {
            return "Hello, " + this.greeting;
        }
    }
    
    console.log(new Greeter("world").greet());
    
    /* 출력 
    Greeter { greet: [Function (anonymous)] }
    greet
    {
      value: [Function (anonymous)],
      writable: true,
      enumerable: false,
      configurable: true
    }
    Hello, world
    */
    

    위 코드에서, enumarable데코레이터는 Greeter클래스의 greet()함수에 enumerable : false속성을 추가합니다.

     

    Accessor Decorators

    접근자 데코레이터는 접근자 선언 직전에 위치합니다. 접근자의 속성 기술자에 적용됩니다. 관찰, 수정, 접근자의 정의를 대체하는데 사용됩니다.

    그러나 선언 파일이나 포위된 컨텍스트에는 사용할 수 없습니다.

    접근자 데코레이터는 단일 멤버에 대해, getter와 setter 둘 다 데코레이팅 하는것을 허용하지 않습니다. 대신, 단일 멤버에 대한 모든 데코레이터는 문서상 첫번째 접근자에만 데코레이팅 해야 합니다. (즉 foo라는 멤버에 대해 setter와 getter가 있고 setter를 먼저 선언했다면, setter에만 데코레이터를 씁니다.)

    접근자 데코레이터는 "다음 세가지 인자를 받는 함수"로서 런타임에 호출됩니다.

    1. 정적 멤버를 위한 클래스의 생성자 함수 혹은 인스턴스 멤버를 위한 클래스의 프로토타입 (뭔소리여)
    2. 멤버의 이름
    3. 멤버의 속성 기술자 (target이 ES5이하라면 undefined입니다.)

    만약 메소드 데코레이터가 값을 반환한다면, 그 값은 속성 기술자로서 사용됩니다. (target이 ES5이하라면 반환값은 무시됩니다.)

     

    function configurable(value: boolean) {
        return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
            descriptor.configurable = value;
            console.log(target);
            console.log(propertyKey);
            console.log(descriptor);
        };
    }
    
    class Point {
        private _x: number;
        private _y: number;
        constructor(x: number, y: number) {
            this._x = x;
            this._y = y;
        }
    
        @configurable(false)
        get x() { return this._x; }
    
        @configurable(false)
        get y() { return this._y; }
    }
    
    const point:Point = new Point(10, 20)
    console.log(point.x, point.y);
    
    /* 출력
    Point { x: [Getter], y: [Getter] }
    x
    {
      get: [Function: get],
      set: undefined,
      enumerable: true,
      configurable: false
    }
    Point { x: [Getter], y: [Getter] }
    y
    {
      get: [Function: get],
      set: undefined,
      enumerable: true,
      configurable: false
    }
    10 20
    */

    위 코드에서, configurable데코레이터는 Point클래스의 xy속성에 configurable: false속성을 추가합니다.

     

    Property Decorators

    속성 데코레이터는 속성 선언 바로 직전에 위치합니다. 

    그러나 선언 파일이나 포위된 컨텍스트에서는 사용할 수 없습니다.

    속성 데코레이터 표현식은 "다음 두가지 인자를 받는 함수"로서 런타임에 호출됩니다.

    1. 정적 멤버를 위한 클래스의 생성자 함수 혹은 인스턴스 멤버를 위한 클래스의 프로토타입 (뭔소리여)
    2. 멤버의 이름

    TS에서 속성 기술자가 초기화되는 방법때문에 속성 데코레이터의 인자에 속성 기술자가 없습니다.

     

    import "reflect-metadata";
    
    const formatMetadataKey = Symbol("format");
    
    function format(formatString: string) {
        console.log(formatString);
        return Reflect.metadata(formatMetadataKey, formatString);
    }
    
    function getFormat(target: any, propertyKey: string) {
        console.log(target);
        console.log(propertyKey);
        return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
    }
    
    class Greeter {
        @format("Hello, %s")
        greeting: string;
    
        constructor(message: string) {
            this.greeting = message;
        }
        greet() {
            let formatString = getFormat(this, "greeting");
            return formatString.replace("%s", this.greeting);
        }
    }
    
    console.log(new Greeter('world').greet());
    
    /* 출력
    Hello, %s
    Greeter { greeting: 'world' }
    greeting
    Hello, world
    */

    위 코드는 Greeter클래스의 greeting속성에 format데코레이터를 적용했습니다. 이 때, Reflect객체를 이용하여 formatString을 메타데이터로 설정하고, greet()함수를 호출할 때 geeting속성의 메타데이터를 가져와 적절히 수정 후 반환합니다.

     

    Parameter Decorators

    파라미터 데코레이터는 파라미터 선언 직전에 위치합니다. 클래스 생성자 함수나 메소드 선언에 적용됩니다. 

    그러나 선언 파일이나 오버로드, 포위된 컨텍스트에는 사용할 수 없습니다.

    파라미터 데코레이터는 메소드에 이미 선언된 파라미터를 관찰하는데에만 사용할 수 있습니다.

    파라미터 데코레이터 표현식은 "다음 세가지 인자를 받는 함수"로서 런타임에 호출됩니다.

    1. 정적 멤버를 위한 클래스의 생성자 함수 혹은 인스턴스 멤버를 위한 클래스의 프로토타입
    2. 멤버의 이름
    3. 함수의 매개변수 목록의 매개변수들의 서수 인덱스 (뭔소리여)

    파라미터 데코레이터의 반환값은 무시됩니다.

     

    import "reflect-metadata";
    
    const requiredMetadataKey = Symbol("required");
    
    function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
        const existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
        existingRequiredParameters.push(parameterIndex);
        Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
    
        console.log(target);
        console.log(propertyKey);
        console.log(parameterIndex);
    }
    
    function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
        const method = descriptor.value;
        descriptor.value = function() {
            const requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
            if (requiredParameters) {
                for (const parameterIndex of requiredParameters) {
                    console.log(parameterIndex);
                    if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                        throw new Error('Missing required argument.');
                    }
                }
            }
    
            return method.apply(this, arguments);
        };
    }
    
    class Greeter {
        greeting: string;
    
        constructor(message: string) {
            this.greeting = message;
        }
    
        @validate
        greet(@required name: string) {
            return "Hello " + name + ", " + this.greeting;
        }
    }
    
    const greeter = new Greeter('world');
    console.log(greeter.greet('einere'));
    console.log(greeter.greet());
    
    /* 출력
    Greeter {}
    greet
    0
    0
    Hello einere, world
    0
    /Users/einere/WebstormProjects/web_example/typescript/tsc/test.js:33
                        throw new Error('Missing required argument.');
                        ^
    
    Error: Missing required argument.
        at Greeter.descriptor.value (/Users/einere/WebstormProjects/web_example/typescript/tsc/test.js:33:27)
        at Object.<anonymous> (/Users/einere/WebstormProjects/web_example/typescript/tsc/test.js:57:21)
        at Module._compile (internal/modules/cjs/loader.js:1151:30)
        at Object.Module._extensions..js (internal/modules/cjs/loader.js:1171:10)
        at Module.load (internal/modules/cjs/loader.js:1000:32)
        at Function.Module._load (internal/modules/cjs/loader.js:899:14)
        at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
        at internal/main/run_main_module.js:17:47
    
    */

    엄청나게 복잡해보이지만, greet()함수의 인자인 namerequired데코레이터를 이용해 해당 매개변수가 필수라는 메타데이터를 적용합니다.

    그리고 validate()라는 함수 데코레이터에서 greet()함수를 호출하기 전에 인자의 개수가 맞는지 확인하여, 맞지 않다면 에러를 던집니다.

     

     

    참고

    https://www.typescriptlang.org/docs/handbook/type-inference.html

     

    Type Inference · TypeScript

    Introduction # In this section, we will cover type inference in TypeScript. Namely, we’ll discuss where and how types are inferred. Basics # In TypeScript, there are several places where type inference is used to provide type information when there is no e

    www.typescriptlang.org

    https://www.typescriptlang.org/docs/handbook/type-compatibility.html

     

    Type Compatibility · TypeScript

    Introduction # Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing. Consider the following code: interface Named { name: stri

    www.typescriptlang.org

    https://www.typescriptlang.org/docs/handbook/decorators.html

     

    Decorators · TypeScript

    Introduction # With the introduction of Classes in TypeScript and ES6, there now exist certain scenarios that require additional features to support annotating or modifying classes and class members. Decorators provide a way to add both annotations and a m

    www.typescriptlang.org

    https://typescript-kr.github.io/

     

    TypeScript 한글 문서

    TypeScript 한글 번역 문서입니다

    typescript-kr.github.io

     

    'programing > Language' 카테고리의 다른 글

    [TS] Interface vs Type  (0) 2020.02.20
    [TS] TypeScript 기초 - 6  (0) 2020.02.01
    [TS] TypeScript 기초 - 4  (0) 2020.01.28
    [TS] TypeScript 기초 - 3  (0) 2020.01.26
    [TS] TypeScript 기초 - 2  (0) 2020.01.25

    댓글

Designed by black7375.