devlog.akasai

Typescript 4.1


typescript

11월 19일 Typescript 4.1이 릴리즈되었다.

한달이 지난 시점이지만, 대략적인 내용을 정리해 보았다.


Template Literal Types

String literal types

Typescript에서는 문자열 나열형태의 타입을 지원한다.

function setVerticalAlignment(pos: "top" | "middle" | "bottom") {
    // ...
}

setVerticalAlignment("middel")
// error: Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.

위 처럼 활용하면 파라미터의 스펠체크가 가능한 형태의 타입이였다.

이를 활용해서 좀 더 다양한 타입정의를 할 수 있다.

type Options = {
    [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
}
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   }

Template literal string type

Javascript템플릿 스트링을 활용하여 TS4.1에서는 좀 더 다양한 타입을 선언할 수 있게 되었다.

type World = "world"

type Greeting = `hello ${World}`
// same as
//   type Greeting = "hello world"

이를 응용하면 길게만 느껴진 나열형태의 타입선언이 아주 간단하게 가능하다.

type VerticalAlignment = "top" | "middle" | "bottom"
type HorizontalAlignment = "left" | "center" | "right"

declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void
//   | "top-left"    | "top-center"    | "top-right"
//   | "middle-left" | "middle-center" | "middle-right"
//   | "bottom-left" | "bottom-center" | "bottom-right"

setAlignment("top-left")   // works!
setAlignment("top-middel") // error!

총 9가지의 타입이 단 두개의 타입과 템플릿 스트링을 활용하여 간단하게 선언가능하다.

Dynamically template literal type

지금까지 알아본 내용들을 이용하면 다양한 형태의 선언이 가능하다.

type PropEventSource<T> = {
    on<K extends string & keyof T>
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
}

declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

let person = makeWatchedObject({
  firstName: 'John',
  age: 42,
})

makeWatchedObject()함수는 obj 파라미터의 타입 T를 동적으로 할당한다.

person 이라는 익명함수를 정의할 때 위와같이 obj를 정의하면 프로퍼티들의 타입을 바탕으로 한 제너릭 타입 T

정의된다.

enum1

이제 PropEventSource type을 바탕으로 on함수를 호출해 볼 수 있다.

// Error! 
person.on("lastNameChanged", _ => {
  // ...
})

// works! 'newName' is typed as 'string'
person.on("firstNameChanged", (newName) => {
  // 'newName' has the type of 'firstName'
  console.log(`new name is ${newName.toUpperCase()}`)
})

// works! 'newAge' is typed as 'number'
person.on("ageChanged", (newAge) => {
  if (newAge < 0) {
    console.log("warning! negative age")
  }
})

위 예제에선 lastNameChanged, firstNameChanged, ageChanged 등 3개의 eventName을 받는다.

on()함수의 파라미터는 ${K}Changed라는 템플릿 스트링 타입이며 K는 제너릭 타입 K extends string & keyof T과 같다.

즉, eventName의 타입은 makeWatchedObject()의 파라미터의 프로퍼티를 바탕으로 정의되므로 **‘firstNameChanged’ | ‘ageChanged’**이다.

따라서 첫번째 예제는 Error이다.


두번째 예제에서는 KfirstName이 된다.

PropEventSource에서 선언된 내용에 따라 on()함수의 콜백 파라미터는 T[K]가 된다.

여기서 T[K]는 **‘John’**이므로 자연스럽게(?) newName은 string타입으로 정의된다.


세번째 예제에서 Kaged이며 on()함수의 콜백 파라미터는 T[K]25이므로

newAge는 number타입으로 정의된다.


Key Remapping in Mapped Types

as 키워드를 이용해 좀 더 명확한 **매핑타입(Mapping Type)**을 선언할 수 있게 되었다.

기존에 주로사용되던 매핑타입은 대표적으로 optional 프로퍼티를 만드는 Partial이 있다.

type Partial<T> = {
    [K in keyof T]?: T[K]
}

TS4.1에서는 위 구문에 as 키워드를 사용하여 좀 더 정교한 타입정의가 가능하다.

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person {
    name: string
    age: number
    location: string
}

type LazyPerson = Getters<Person>

const person: LazyPerson = {
  getName: () => 'John',
  getAge: () => 25,
  getLocation: () => 'seoul'
}

위 예제에서 Getters타입은 제너릭 T의 key값들을 이용하여 get${K}형태의 익명함수로 구성된 타입이다.

interface Person을 제너릭타입으로 하는 LazyPerson타입을 통해

각 키값을 바탕으로 getName | getAge | getLocation이라는 익명함수를 갖는 person변수를 선언할 수 있다.


ETC

이 외에도 다양한 변경점이 존재한다.

  1. abstract Members Can’t Be Marked async

    추상 멤버는 더 이상 async 키워드를 기입하지 않아도 된다.

  2. any/unknown Are Propagated in Falsy Positions

    declare let foo: unknown
    declare let somethingElse: { someProp: string }
    
    let x = foo && somethingElse

    기존에는 any/unknown 타입은 false로 판단되어 위 예제에서 xsomethingElse으로 정의된다.

    하지만 TS4.1에서는 더 이상 false로 판단되지 않으므로 unknown타입으로 정의된다.

    그러므로 any/unknown타입을 boolean값으로 활용하려면 !!foo && someExpression와 같이 정해야한다.

  3. resolve‘s Parameters Are No Longer Optional in Promises

    Promise함수에서 resolve()함수의 인자값이 Required로 변경된다.

    new Promise<number>((resolve) => {
        //     ^^^^^^^^
        doSomethingAsync((value) => {
           doSomething()
           resolve(value)
           //      ^^^^^
       })
    })

    만약 아무 인자값이 없다면 void타입으로 정의해야한다.

  4. The new type aliases Uppercase, Lowercase, Capitalize and Uncapitalize

    새로운 내장 앨리어스타입이 추가되었다.

    명칭 그대로 대문자, 소문자와 관련된 타입들이다.


Summary

Typescript는 버전마다 다양한 기능들이 추가된다.

사실 딥하게 사용하지 않는다면 크게 와닿는 기능들은 몇개되지 않는다.

하지만 내용을 알고 있다면 분명 응용할 수 있는 부분들이 생길 것이라고 생각한다.

특히 Template literal type은 상당히 인상깊었고 꼭 한번 활용해보려고 한다.

더 많은 내용들이 컬럼에 정리되어 있으니 관심있다면 정독해보는 것도 좋을 것 같다.


Reference


  • akasai

    👨‍💻 Backend Developer

    • #Node.js
    • #Typescript
    • #GraphQL
    • #Serverless
    • #PostgreSQL
    • #Kubernetes