jgjgill

러닝 타입스크립트 CHAPTER 14 - 구문 확장

7 min read
learing-typescript-thumbnail

CHAPTER 14 구문 확장

타입스크립트와 같은 상위 집합 언어에 특정 새로운 런타임 기능으로 자바스크립트 구문을 확장하는 방식은 다음과 같은 이유로 나쁜 사례로 간주합니다.

  • 런타임 구문 확장이 최신 버전 자바스크립트의 새로운 구문과 충돌할 수 있다.
  • 언어를 처음 접하는 프로그래머가 자바스크립트가 끝나는 곳과 다른 언어가 시작하는 곳을 이해하기 어렵게 만든다.
  • 상위 집합 언어 코드를 사용하고 자바스크립트를 내보내는 트랜스파일러의 복잡성을 증가시킨다.

따라서 타입스크립트 언어로 자바스크립트의 세 가지 구문 확장을 도입했다는 것에 무거운 마음과 깊은 유감을 갖습니다.

  • 클래스: 사양이 승인됨에 따라 자바스크립트 클래스에 맞춘 클래스
  • 열거형: 키와 값의 일반 객체와 유사한 간단한 구문
  • 네임스페이스: 코드를 구조화하고 배열하는 최신 모듈보다 앞선 해결책

클래스 매개변수 속성

클래스를 많이 사용하는 프로젝트나 클래스 이점을 갖는 프레임워크가 아니라면 클래스 매개변수 속성을 사용하지 않는 것이 좋습니다.

타입스크립트는 매개변수 속성을 선언하기 위한 단축 구문을 제공합니다. 속성은 클래스 생성자의 시작 부분에 동일한 타입의 멤버 속성으로 할당됩니다. 생성자의 매개변수 앞에 readonly 또는 public, protected, private 제한자 중 하나를 배치하면 타입스크립트가 동일한 이름과 타입의 속성도 선언하도록 지시합니다.

class Engineer {
  constructor(readonly area: string) {
    console.log(`I work in the ${area} area.`)
  }
}

// 타입 string
new Engineer('mechanical').area

다음 NamedEngineer 클래스는 일반 속성 fullName, 일반 매개변수 name, 매개변수 속성 area를 선언합니다.

class NamedEngineer {
  fullName: string

  constructor(name: string, public area: string) {
    this.fullName = `${name}, ${area} engineer`
  }
}

매개변수 속성이 없을 시에는 area를 명시적으로 할당하기 위해 코드가 몇 줄 추가됩니다.

class NamedEngineer {
  fullName: string
  area: string

  constructor(name: string, area: string) {
    this.area = area
    this.fullName = `${name}, ${area} engineer`
  }
}

대부분의 프로젝트에서는 런타임 구문 확장으로 앞에서 언급했던 단점으로 인해 매개변수를 완전히 사용하지 않는 것을 선호합니다. 또한 매개변수 속성은 새로운 # 클래스 private 필드 구문과 함께 사용할 수 없습니다.

반면에 클래스 생성을 매우 선호하는 프로젝트에서는 매개변수 속성을 사용하면 좋습니다. 매개변수 속성은 매개변수 속성 이름과 타입을 두 번 선언해야 하는 편의 문제를 해결하는데, 이 선언은 자바스크립트가 아닌 타입스크립트 고유의 것입니다.

실험적인 데코레이터

클래스를 포함하는 많은 다른 언어에서는 클래스와 클래스의 멤버를 수정하기 위한 일종의 런타임 로직으로 주석을 달거나 데코레이팅할 수 있습니다.

@myDecorator
class MyClass {
  /* ... */
}

데코레이터의 각 사용법은 데코레이팅하는 엔티티가 생성되자마자 한 번 실행됩니다. 각 종류의 데코레이터(접근자, 클래스, 메서드, 매개변수, 속성)는 데코레이팅하는 엔티티를 설명하는 서로 다른 인수 집합을 받습니다.

다음 Greeter 클래스 메서드에서 사용된 logOnCall 데코레이터는 Greeter 클래스 자체와 속성(log)의 키, 그리고 속성을 설명하는 descriptor 객체를 받습니다. Greeter 클래스에서 원래의 greet 메서드를 호출하기 전에 descriptor.value를 수정해 greet 메서드를 데코레이팅합니다.

function logOnCall(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value
  console.log('[logOnCall] I am decorating', target.constructor.name)

  descriptor.value = function (...args: unknown[]) {
    console.log(`[descriptor.value] Calling ${key} with:`, ...args)
    return original.call(this, ...args)
  }
}

class Greeter {
  @logOnCall
  greet(message: string) {
    console.log(`[greet] Hello, ${message}!`)
  }
}

new Greeter().greet('you')
// Output log:
// "[logOnCall] I am decorating",  "Greeter"
// "[descriptor.value] Calling greet with:",  "you"
// "[greet] Hello, you!"

열거형

자주 반복되는 리터럴 집합이 있고, 그 리터럴 집합을 공통 이름으로 설명할 수 있으며, 열거형으로 전환했을 때 훨씬 더 읽기 쉬운 경우가 아니라면 열거형을 사용해서는 안됩니다.

대부분의 프로그래밍 언어는 연관된 값 집합을 나타내는 enum 또는 열거형 타입의 개념을 포함합니다. 열거형은 각 값에 대해 친숙한 이름을 사용한 객체에 저장된 리터럴 값 집합으로 생각할 수 있습니다.

자바스크립트는 열거형 구문을 포함하지 않으므로 열거형을 배치해야 하는 곳에 일반적인 객체를 사용합니다.

const StatusCodes = {
  InternalServerError: 500,
  NotFound: 404,
  Ok: 200
} as const

StatusCodes.InternalServerError // 500

열거형같은 객체를 사용할 때 까다로운 점은 값이 해당 객체의 값 중 하나여야 함을 나타내는 훌륭한 타입 시스템 방법이 없다는 것입니다. 한 가지 일반적인 방법은 keyoftypeof 타입 제한자를 함께 사용해 하나의 값을 해킹하는 것이지만, 이는 상당한 양의 구문을 입력해야 합니다.

const StatusCodes = {
  InternalServerError: 500,
  NotFound: 404,
  Ok: 200
} as const

// 타입: 200 | 404 | 500
type StatusCodeValue = (typeof StatusCodes)[keyof typeof StatusCodes]

let statusCodeValue: StatusCodeValue

statusCodeValue = 200 // Ok
statusCodeValue = -1
// Error: Type '-1' is not assignable to type 'StatusCodeValue'.

타입스크립트는 타입이 number 또는 string인 리터럴 값들을 갖는 객체를 생성하기 위한 enum구문을 제공합니다.

enum StatusCode {
  InternalServerError = 500,
  NotFound = 404,
  Ok = 200
}

StatusCode.InternalServerError // 500

컴파일된 자바스크립트에서 열거형은 이에 상응하는 객체로 컴파일됩니다.

enum StatusCode는 대략 다음과 같은 자바스크립트 코드를 생성합니다.

var StatusCode
;(function (StatusCode) {
  StatusCode[(StatusCode['InternalServerError'] = 500)] = 'InternalServerError'
  StatusCode[(StatusCode['NotFound'] = 404)] = 'NotFound'
  StatusCode[(StatusCode['Ok'] = 200)] = 'Ok'
})(StatusCode || (StatusCode = {}))

열거형은 '자바스크립트에 새로운 런타임 구문 구조를 절대 추가하지 않는다'는 타입스크립트의 일반적인 만트라를 위반합니다. 하지만, 다른 한편으로 알려진 값 집합을 명시적으로 선언하는데 열거형은 매우 유용합니다.

자동 숫자값

열거형의 멤버는 명시적인 초깃값을 가질 필요가 없습니다. 값이 생략되면 첫번째 값을 0으로 시작하고 후속 값을 1씩 증가시킵니다.

enum VisualTheme {
  Dark, // 0
  Light, // 1
  System // 2
}

문자열 값을 갖는 열거형

열거형은 멤버로 숫자 대신 문자열 값을 사용할 수 있습니다.

enum LoadStyle {
  AsNeeded = 'as-needed',
  Eager = 'eager'
}

문자열 값을 갖는 열거형은 읽기 쉬운 이름으로 공유 상수의 별칭을 지정하는데 유용합니다.

문자열 값의 단점은 타입스크립트에 따라 자동으로 계산할 수 없다는 것입니다. 숫자값이 있는 멤버 뒤에 오는 열거형 멤버만 자동으로 계산할 수 있습니다.

enum Wat {
  FirstString = 'first',
  SomeNumber = 9000,
  ImplicitNumber, // OK (value 9001)
  AnotherString = 'another',
  NotAllowed
  // Error: Enum member must have initializer.
}

const 열거형

열거형은 런타임 객체를 생성하므로 리터럴 값 유니언을 사용하는 일반적인 전략보다 더 많은 코드를 생성합니다. 타입스크립트는 const 제한자로 열거형을 선언해 컴파일된 자바스크립트 코드에서 객체 정의와 속성 조회를 생략하도록 지시합니다.

const enum DisplayHint {
  Opaque = 0,
  Semitransparent,
  Transparent
}

let displayHint = DisplayHint.Transparent

컴파일된 자바스크립트 코드에는 열거형 선언이 모두 누락되고 열거형의 값에 대한 주석을 사용합니다.

let displayHint = 2 /* DisplayHint.Transparent */

네임스페이스

기존 패키지에 대한 DefinitelyTyped 타입 정의를 작성하지 않는 한 네임스페이스를 사용하지 마세요. 네임스페이스는 최신 자바스크립트 모듈 의미 체계와 일치하지 않습니다. 자동 멤버 할당은 코드를 읽는 것을 혼란스럽게 만들 수 있습니다. .d.ts 파일에서 네임스페이스를 접할 수 있기 때문에 이번 절에서 언급할 뿐입니다.

ECMA스크립트 모듈이 승인되기 전에는 웹 애플리케이션이 출력 코드 대부분을 브라우저에 따라 로드되는 하나의 파일로 묶는 것이 일반적이었습니다.

타입스크립트 언어는 지금은 네임스페이스라 부르는 내부 모듈 개념을 가진 하나의 해결책을 제공했습니다.

namespace Randomized {
  const value = Math.random()
  console.log(`My value is ${value}`)
}

네임스페이스 내보내기

콘텐츠를 네임스페이스 객체의 멤버로 만들어 내보내는 기능 덕분에 코드의 다른 영역에서 네임스페이스 이름으로 해당 멤버를 참조할 수 있습니다.

namespace Settings {
  export const name = 'My Application'
  export const version = '1.2.3'
  export function describe() {
    return `${Settings.name} at version ${Settings.version}`
  }
  console.log('Initializing', describe())
}
console.log('Initialized', Settings.describe())

중첩된 네임스페이스

네임스페이스는 다른 네임스페이스 내에서 네임스페이스를 내보내거나 하나 이상의 마침표(.)를 사용해서 무한으로 중첩할 수 있습니다.

다음 두 개의 네임스페이스 선언은 동일하게 작동합니다.

namespace Root.Nested {
  export const value1 = true
}

namespace Root {
  export namespace Nested {
    export const value2 = true
  }
}

중첩된 네임스페이스는 네임스페이스로 구성된 더 큰 프로젝트의 구역들 사이에 더 자세한 설명을 적용할 수 있는 편리한 방법입니다.

타입 정의에서 네임스페이스

네임스페이스는 DefinitelyTyped 타입 정의에 유용합니다.

네임스페이스보다 모듈을 선호함

네임스페이스로 구조화된 타입스크립트 코드는 웹펙과 같은 빌더에서 사용하지 않는 파일을 제거하는 것이 쉽지 않습니다. 네임스페이스는 ECMA스크립트 모듈처럼 파일 간에 명시적으로 선언되는게 아니라 암시적으로 연결을 생성하기 때문입니다. 따라서 타입스크립트 네임스페이스가 아닌 ECMA스크립트 모듈을 사용해 런타임 코드를 작성하는 것이 좋습니다.

타입 전용 가져오기와 내보내기

타입 전용 가져오기와 내보내기는 자바스크립트 출력에 어떠한 복잡성도 추가하지 않습니다.

// index.ts
import { type TypeOne, value } from 'my-example-types'
import type { TypeTwo } from 'my-example-types'
import type DefaultType from 'my-example-types'

export { type TypeOne, value }
export type { DefaultType, TypeTwo }

// index.js
import { value } from 'my-example-types'
export { value }

가져오기가 타입 전용으로 표시된 경우, 이를 런타임 값으로 사용하려고 하면 타입스크립트 오류가 발생합니다.

import { ClassOne, type ClassTwo } from 'my-example-types'
new ClassOne() // OK

new ClassTwo()
// Error: 'ClassTwo' cannot be used as a value because it was imported using 'import type'.

마치며

  • 클래스 생성자에서 클래스 매개변수 속성 선언하기
  • 인수 클래스와 필드에 데코레이터 사용하기
  • 열거형으로 값 그룹 나타내기
  • 파일 또는 타입 정의에 그룹화 생성을 위해 네임스페이스 사용하기
  • 타입 전용 가져오기와 내보내기
@2023 powered by jgjgill