ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] TypeScript 기초 - 4
    programing/Language 2020. 1. 28. 22:49

    안녕하세요, 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


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

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

     

     

    Generic

    자바를 하신분들에게 매우 익숙하실 제네릭입니다. TS에서의 제네릭은 자료형을 특정 문자열로 치환하는 것이라고 생각하시면 될 것 같습니다.

     

    Generic Type Variables

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

    숫자형 arg를 받아 숫자형 arg를 반환하는 함수입니다.

    파라미터의 자료형과 반환값으 자료형이 둘다 number이므로, 중복을 좀 최소화하고 하드코딩을 줄여봅시다.

     

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

    any 자료형을 이용해 추상화를 높이긴 했지만, 반환값이 무엇인지 알 수 없게 되어버렸습니다.

    입력값의 자료형과 반환값의 자료형이 달라도 에러가 나지 않으니까요.

     

    function identity<T>(arg: T): T {
        return arg;
    }
    
    identify<number>(10);
    identify<string>('hello');
    // Argument of type '"hello"' is not assignable to parameter of type 'number'.
    identify<number>('hello');

    그러나 제네릭을 쓰면, 입력값으로 어떤 타입이던 받을 수 있고, 반환값은 입력값과 같은 자료형임을 알 수 있습니다.

    제네릭은 이처럼 개방폐쇠의 원칙을 지키기 좋은 도구입니다.

    문법은 꺾쇠괄호 사이에 추상화 하고자 하는 자료형 (보통 T, U, V... 이런식으로 많이 쓰는 것 같아요.)을 쓰시고, 해당 코드 내부에서 자유롭게 사용하시면 됩니다.

    또한 제네릭을 사용하실 땐, 반드시 꺽쇠괄호 내부에 타입을 명시해주시는것이 좋습니다.

     

    interface MyType {
        foo: string;
        bar: number;
    }
    
    function identity<T>(arg: T): T {
        return arg;
    }
    
    identity<MyType>({ foo: 'hello', bar: 10 });
    

    물론, 원시가 아닌 값도 제네릭으로 사용할 수 있습니다.

     

    Generic Types

    function identity<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;

    혹은 인터페이스에도 제네릭을 사용할 수 있습니다.

     

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

    클래스에도 제네릭을 사용할 수 있습니다.

     

    Generic Constraints

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

    위 코드는 제네릭 타입으로 받는 변수 arg가 length속성을 가지고 있는지 보장하지 않습니다.

     

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

    그러나 위 코드처럼, 제네릭 타입에 대한 상속을 이용하면 제네릭에 대한 제약조건을 걸 수 있습니다.

     

    interface Person {
        name: string;
        age: number;
    }
    
    // 예시에서 K는 T의 키들의 유니온이 됩니다. 
    // T가 Person인 경우, K는 "name" | "age"을 상속하게 됩니다.
    function getPersonProperty<T, K extends keyof T> (obj: T, key: K): T[K] {
        return obj[key];
    }
    function setPersonProperty<T, K extends keyof T > (obj: T, key: K, value: T[K]): void {
        obj[key] = value;
    }
    
    const person: Person = {
        name: 'foo',
        age: 10,
    };
    
    getPersonProperty(person, 'name');         // 'foo'
    getPersonProperty(person, 'age');          // 10
    getPersonProperty(person, 'gender');       // Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'.
    setPersonProperty(person, 'name', 'bar');  // {name: 'bar', age: 10}
    setPersonProperty(person, 'age', 20);      // {name: 'bar', age: 20}
    setPersonProperty(person, 'gender', true); // Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'.

    주로 어떤 객체에 특정 프로퍼티가 존재하는것을 보장하고 싶을 때 많이 사용합니다. (객체에 특정 프로퍼티가 없어서 undefined뜨는 경우 많이 겪어보셨을 거에요..)

     

    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>(class: new () => A): A {
        return new class();
    }
    
    createInstance(Lion).keeper.nametag;  // typechecks!
    createInstance(Bee).keeper.hasMask;   // typechecks!

    혹은 위와 같이 특정 부모 클래스(Animal)를 상속한 클래스(Bee, Lion)만 받도록 할 수도 있습니다.

     

     

    Enums

    열거형은 유명상수의 집합을 정의할 때 사용합니다. 이를 이용해서 switch문에 유용하게 사용할 수 있습니다.

    TS에서는 numeric-based enums와 string-based enums를 지원합니다.

     

    Numeric Enums

    // 모든 멤버가 이니셜라이저가 없습니다.
    enum Direction {
        Up,  // 0
        Down, // 1
        Left, // 2
        Right, // 3
    }
    Direction.Up // 0
    Direction.Right // 3
    
    // Down과 Right만 이니셜라이저가 있습니다.
    enum Direction {
        Up = 1,
        Down, // 2
        Left = 10,
        Right,  // 11
    }
    
    Direction.Up // 1
    Direction.Right // 11

    열거형은 기본적으로 위에서부터 차례대로 0, 1, 2, ...순으로 값이 부여가 됩니다.

    만약 멤버들의 값을 설정하고 싶다면 정수를 대입하시면 됩니다.

    숫자 열거형의 값은 계산된 값이거나 상수입니다. 자세한 내용은 Computed and constant members를 참고해주세요.

     

    String Enums

    enum Direction {
        Up = "UP",
        Down = "DOWN",
        Left = "LEFT",
        Right = "RIGHT",
    }

    문자 열거형은 숫자 열거형과 비슷하지만, 런타임에 미묘한 차이가 있습니다.

    문자 열거형의 모든 멤버는 스트링 리터럴 혹은 다른 문자 열거형의 멤버의 값으로 초기화되어야 합니다.

    문자 열거형은 숫자 열거형과 달리 auth-incrementing이 없으므로, 직렬화에 대해 이점이 있습니다. 예를 들어, 런타임에 값을 읽으려고 할 때, 숫자 열거형은 값이 동적이어서 예측하기가 힘들지만, 문자 열거형은 동적이여서 예측이 가능하여 의미있고 가독성이 좋습니다.

     

    Computed and constant members

    다음 세가지 경우는 상수 멤버로 취급합니다.

    1. 열거형의 첫번째 멤버가 이니셜라이저가 없는 경우, 상수 0으로 취급합니다.
    2. 열거형의 멤버가 이니셜라이저가 없고 이전 멤버가 숫자형 상수인 경우, 이전 멤버의 값 + 1인 상수 취급합니다.
    3. 열거형의 멤버가 상수 열거형 표현식인 경우. (자세한 내용은 공식 문서를 참고해주세요.) (https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)

    상수 멤버가 아닌 멤버는 모두 계산된 멤버로 간주합니다.

     

    Heterogeneous enums

    enum BooleanLikeHeterogeneousEnum {
        No = 0,
        Yes = "YES",
    }

    하나의 열거형의 멤버는 숫자형이거나 문자형일 수 있지만, 권장되지는 않습니다.

     

    Union enums and enum member types

    리터럴 열거형 멤버는 초기화 값이 없는 상수멤버이거나 다음 값들로 초기화된 멤버를 의미합니다.

    • 문자열 리터럴
    • 숫자 리터럴 (음수 포함)

    열거형의 모든 멤버가 리터럴 열거형 멤버일 경우, 특별한 의미를 가지고 동작하게 됩니다.

    // ShapeKind 열거형의 멤버는 자료형이 될 수 있습니다.
    enum ShapeKind {
        Circle, // 0
        Square, // 1
    }
    
    interface Circle {
        kind: ShapeKind.Circle;
        radius: number;
    }
    
    interface Square {
        kind: ShapeKind.Square;
        sideLength: number;
    }
    
    let c: Circle = {
        kind: ShapeKind.Square, // Error! Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
        radius: 100,
    }

    첫번째 특별한 동작은, 열거형의 각 멤버는 자료형으로 취급될 수 있습니다.

    위 코드에서, Circle과 Square는 자료형으로 취급될 수 있기 때문에, Circle형 변수 c를 선언할 수 있으머 kind의 자료형으로서 Square를 쓸 수 있습니다.

     

    enum E {
        Foo,
        Bar,
    }
    
    function f(x: E) {
        if (x !== E.Foo || x !== E.Bar) {
            //             ~~~~~~~~~~~
            // Error! This condition will always return 'true' since the types 'E.Foo' and 'E.Bar' have no overlap.
        }
    }

    두번째 특별한 동작은, 열거형은 그 자체로서 유니온의 역할을 수행한다는 것입니다.

    위 코드에서, 함수 f의 인자로는 E형 x를 받습니다. if문에서 x가 Foo인지 아닌지 검사합니다. 만약 x가 Foo가 아니라면, 열거형의 특성상 유니온으로 취급되므로, Bar일 수 밖에 없습니다. 따라서 x !== E.Bar는 무의미한 비교문입니다.

     

    Enums at runtime

    열거형은 런타임에서는 실존하는 객체입니다. 따라서 다음과 같은 코드가 동작합니다.

    enum E {
        X, Y, Z
    }
    
    function f(obj: { X: number }) {
        return obj.X;
    }
    
    // Works, since 'E' has a property named 'X' which is a number.
    f(E);

     

    Enums at compile time

    enum LogLevel {
        ERROR, WARN, INFO, DEBUG
    }
    
    // type keyOfLogLevel = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
    type keyOfLogLevel = keyof LogLevel;
    
    // type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
    type LogLevelStrings = keyof typeof LogLevel;
    
    function printImportant(key: LogLevelStrings, message: string) {
        const num = LogLevel[key];
        if (num <= LogLevel.WARN) {
           console.log('Log level key is: ', key);
           console.log('Log level value is: ', num);
           console.log('Log level message is: ', message);
        }
    }
    printImportant('ERROR', 'This is a message');

    열거형은 런타임때 객체이므로, keyof 키워드를 사용한다면 Object의 속성들의 키값들의 유니온 자료형이 나옵니다. 따라서 keyOfLogLevel"toString" | "toFixed" ...와 같은 유니온 자료형이 되어버립니다.

    그러나 typeof LogLevel은 리터럴 객체({ ERROR, WARN, ...})이기 때문에(제 예상입니다), keyof typeof LogLevel은 리터럴 객체의 속성들의 키값들의 유니온 자료형이 됩니다.

    (그런데 개인적으로 왜 컴파일타임이라는 용어를 쓴건지 잘 모르겠네요..)

     

    Ambient enums

    포위된 열거형(번역이 올바른지 모르겠네요)은 이미 존재하는 열거형 자료형의 형태를 정의하는데 사용됩니다.

    (declare 키워드를 사용하면 포위된 선언을 만들 수 있다고 하네요.)

    declare enum Enum {
        A = 1,
        B,
        C = 2
    }

    위에서 상수 멤버와 계산된 멤버를 얘기해드렸죠? 특수한 세가지 경우에 대해서는 상수 멤버로 취급된다고 했습니다.

    그런데 이니셜라이저가 없는 포위된 열거형의 멤버는 항상 계산된 멤버로 취급합니다.

     

     

    참고

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

     

    Generics · TypeScript

    Introduction # A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you

    www.typescriptlang.org

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

     

    Enums · TypeScript

    Enums # Enums allow us to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases. TypeScript provides both numeric and string-based enums. Numeric enums # We’ll first start off with numeric enu

    www.typescriptlang.org

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

     

    TypeScript 한글 문서

    TypeScript 한글 번역 문서입니다

    typescript-kr.github.io

     

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

    [TS] TypeScript 기초 - 6  (0) 2020.02.01
    [TS] TypeScript 기초 - 5  (0) 2020.01.31
    [TS] TypeScript 기초 - 3  (0) 2020.01.26
    [TS] TypeScript 기초 - 2  (0) 2020.01.25
    [TS] TypeScript 기초 - 1  (0) 2020.01.24

    댓글

Designed by black7375.