2019-11-08: 이제 TypeScript 3.7 나왔으니 사용하시면 됩니다.

TypeScript 3.7 출시 임박!

TypeScript의 3.7 베타버전이 공개되었습니다.

Announcing TypeScript 3.7 Beta

JavaScript의 표준인 ECMA-262과 기술 위원회 TC39가 발표하는 사양 업데이트에 따라 TypeScript도 이에 발맞춰 언어 사양을 업데이트 하고 있습니다.

빠르면 내달(2019년 11월)부터 TypeScript 3.7을 정식 버전으로 만나볼 수 있습니다.

이에 앞서 과연 TypeScript 3.7에 무엇이 추가되었고, 앞으로 개발하는데 어떤 이점이 있는지 살펴보겠습니다.


1. Optional Chaining (중요)

오~ 이건 괜찮다!

tc39/proposal-optional-chaining

null이나 undefined는 언제나 까다로운 녀석들입니다.

특히 타입을 체크하는 TypeScript에서는 무시하고 넘어가기도 매우 성가십니다.

예를 들어 이런 객체 구조가 있다고 합시다.

type Something = {
  a?: {
    b?: {
      c: string
    }
  }
}

const something: Something

JavaScript 같았으면 something.a.b.c 해도 문법적으로는 오류가 안 납니다. (하지만 뒷감당을…)

물론 TypeScript에서도 Type Assertion !를 사용해서 something.a!.b!.c 할 수 있습니다. (역시 뒷감당이…)

정직하게 타입을 체크하고 넘어가려면,

if (something.a) {
  if (something.a.b) {
    // something.a.b.c
  }
}

이렇게 하거나,

something.a && something.a.b && something.a.b.c

이렇게 해야 했습니다.

어쨌거나 코드가 많고 귀찮아지기 때문에, 때로는 타입을 뭉개버리고(?) 넘어가고 싶은 유혹에 쉽게 빠지게 됩니다.

하지만 이런 지저분한 부분을 깔끔하고 간단하게 표현할 수 있는 문법적 개선이 이번에 TypeScript 3.7에 추가되었습니다.

이제 이렇게 하면 됩니다.

something.a?.b?.c

위의 중첩된 if문으로 타입 체크를 하는 코드와 같은 역할을 합니다.

만약 something, a, b, c로 내려가는 중에 null이나 undefined를 만나면 그냥 아무 것도 안 하고 끝납니다.

타입 검사를 충족하도록 도와주는 문법적 장치(Syntactic Sugar)인 동시에 제어 흐름상 단축 표현(Short-Circuit) 역할을 합니다.

다른 말로 Safe navigation operator라고도 불립니다.

이것만 해도 매우 영향력이 큰 업데이트가 되겠습니다.


2. Nullish Coalescing (중요)

tc39/proposal-nullish-coalescing

RDBMS에 익숙한 개발자는 COALESCE() (혹은 NVL()) 함수를 어디선가 보았을 것입니다.

NULL이 들어간 레코드의 칼럼 값 대신 '', 0 혹은 기타 다른 대체 값(fallback default)을 넣어주는 기능입니다.

TypeScript에서도 이런 기능을 하는 문법이 추가되었습니다.

이제 이렇게 하시면 됩니다.

const b = a ?? 'default'

a가 undefined 이거나 null이면 ?? 다음의 값이 대신 들어갑니다.

근데 이거 원래부터 있던 기능 아닌가요?

아마 ||를 떠올리면서 똑같은 거 아니냐 하시는 분이 있을 것입니다.

그런데 다릅니다. 똑같은 거였으면 만들 필요가 없지 않겠습니까?

||는 모든 falsy(논리적으로 false로 평가되는 값: '', 0, false, undefined, null, NaN 등)에 대해서 적용되고,

??undefinednull에 대해서만 적용된다는 차이가 있습니다.

예를 들어,

const a1 = 0 || 0.5
const b1 = '' || 'default'

const a2 = 0 ?? 0.5
const b2 = '' ?? 'default'

a1, b1에는 각각 0.5, default가 들어가지만,

a2, b2에는 각각 0, ''이 들어갑니다.

이제 차이점을 아시겠지요?

이것도 꽤 유용할 것 같습니다.


3. Assertion Functions (별로)

tsc 컴파일러의 assert() 함수에 대한 타입 추론 기능이 향상되었습니다.

assert()는 런타임에서 불변 조건(invariant)을 검사하는 함수입니다.

예를 들면 이런 구문을 넣어서 런타임 타입 검사를 할 수 있습니다.

function someFunc(str: any) {
  assert(typeof str === 'string')
}

위에서 파라미터 str의 타입이 string이라고 단언(assertion)을 했으므로(파라미터 타입 선언에는 any이지만) 논리적으로 str이 string 타입임을 추론할 수 있지만,

이전에는 assert() 구문이 타입 추론에 영향을 주지 못했습니다.

function someFunc(str: any) {
  assert(typeof str === 'string')
  // str은 여전히 any로 판정됨
}

그런데 이제 assert()로 타입에 대한 단언을 넣었을 때 컴파일러가 단언문으로부터 타입을 추론할 수 있도록 타입 검사 장치가 추가되었습니다.

사실 원래는 아래와 같이 하면 좋을 거라고 생각했는데, (이 정도는 되어야 개선이지!)

function someFunc(str: any) {
  assert(typeof str === 'string')
  // str은 이 라인부터 string 타입으로 판정됨

  // VSCode에서 str하고 .찍으면 자동완성 기능도 가능
  str.toUpperCase()
}

이렇게는 안 된다고 합니다. -_-

대신 이렇게 해야 제대로 작동합니다.

function assertIsString(str: any): asserts str is string {
  ...
}

asserts <condition> 하는 식으로 리턴 타입에 void 대신 asserts 키워드를 사용해서 컴파일러에 정보를 주는 방법입니다.

음… 그리 많이 사용하지는 않을 것 같습니다. (실망)


4. Better Support for never-Returning Functions (별로)

개발 중에 function의 시그니처만 만들고 본문을 구현하지 않았을 때, 흔히 이렇게 작성합니다.

function someFunc(): string {
  throw new Error('Unimplemented.')
}

이때 함수 리턴 타입이 string이지만 return ''을 본문에 안 넣어도 throw new Error('Unimplemented')가 컴파일 오류가 발생하지 않도록 해줍니다.

왜냐하면 throw 구문이 never 타입을 리턴하고 종료하는 것으로 판정을 하기 때문입니다.

이와같이 특정 부분에 리턴 타입 검사를 우회할 수 있도록 하는 구문들이 있습니다.

위에서는 throw 키워드가 그 역할을 했습니다.

그리고 이번에 3.7 포함된 process.exit()가 있습니다.

process.exit()의 리턴 타입은 원래 never였습니다.

그런데 이번에, 어차피 process.exit()를 하면 종료가 되기 때문에 throw와 마찬가지로 호출하는 그 지점에서 종료되는 것으로 판정(즉 return never)되도록 변경되었습니다.

예를 들어 전에는,

function someFunc(): string {
  ...
  return process.exit(1)
  ...
}

해야 되었는데, 이제 이렇게만 해도 됩니다.

function someFunc(): string {
  ...
  process.exit(1)
  ...
}

근데, 음…

별 쓸데가 없네요 ;;


5. Recursive Type Aliases (중요할 수도?)

타입 선언 규칙이 개선되었습니다.

전에는 이런 타입 선언이 불가능했습니다.

type TreeNode<V> = {
  value: V
  left?: TreeNode<V>
  right?: TreeNode<V>
}

타입 선언 안에 자기 선언을 바로 참조하는 이런 형태를 재귀 선언이라고 하는데(TreeNode 선언 안에서 TreeNode를 참조), 위의 경우와 같이 생각대로 타입 선언이 되지 않아서(타입 오류가 나서) 짜증이 나는 경우가 있었습니다.

타입 정보의 순환 참조 오류(닭이 먼저냐 알이 먼저냐)를 방지하려고 만든 제약인데, 이번에 어떻게 잘 해결이 된 모양입니다.

라이브러리 만드는 개발자들이 반가워할 만한 개선입니다.


6. tsc 컴파일러 옵션에 –declaration, –allowJs를 같이 사용 가능

전에는 --declaration, --allowJs 옵션을 같이 사용하지 못했는데, 이제 같이 사용 가능하게 되었다는 소식입니다.

그런데 저희는 JavaScript를 사용하지 않아서 그냥 그런가보다 하고 있습니다.


7. Build-Free Editing with Project References (중요)

오~ 이건 괜찮다!

Project Reference는 TypeScript 3.0부터 추가된 멀티 프로젝트 기능입니다.

하나의 큰 프로젝트 대신에 여러 개의 서브 프로젝트로 나누고 메인 프로젝트에 reference로 서브 프로젝트를 연결시켜서 개발할 수 있습니다.

전에는 tsc --build --watch 를 실행해서 서브 프로젝트의 내용을 수정하면 reference로 엮인 의존 관계에 따라 자동으로 빌드가 이루어지도록 하는 방법으로 개발을 했습니다.

그런데 이 방법으로는 자동으로 빌드는 되지만 개발 도구와의 연동성이 조금 떨어졌습니다.

예를 들면, VSCode가 서브 프로젝트의 코드가 빌드된 것을 바로 알아차리지 못해서 서브 프로젝트의 코드를 고칠 때마다 TypeScript: Restart TS Server를 실행하는 불편함이 있었습니다.

이제 TypeScript 3.7부터는 서브 프로젝트를 참조할 때 소스코드 파일(ts, tsx)를 직접 참조함으로써, 서브 프로젝트의 변경 사항이 즉시 반영됩니다.

앞으로 개발하기가 더 편리하겠네요.


8. Top-Level await (중요?)

async/await 구분에서 await는 다음과 같이 async 함수 안에서만 사용할 수 있었습니다.

async function someFunc() {
  await ...
}

그런데 이제 module 자체를 (ts파일 본문) 아예 async function이라고 보고,

async function ()으로 감싸지 않고도 module 수준에서 await를 사용할 수 있게 됩니다.

다만, 이 기능은 이슈가 있어서 TypeScript 3.7에 포함이 안 될 수도 있습니다.

이게 잘 되면, Node REPL에서의 코딩과, Code Splitting시에 유리합니다.

이런게 가능합니다.

const LazyLoadableComponent = await import('./components/LazyLoadableComponent')

그냥 import가 아니고 import()를 하면 모듈을 비동기 로딩하게 되고, 번들러는 이 구문을 기준으로 번들 파일을 청킹(chunking)할 수 있습니다.

SPA 프론트엔드 프로젝트에서 번들 파일을 여러 조각으로 나누는 것이 일반적이므로 이것을 더 쉽게 할 수 있도록 해줍니다.


결론

빠르게 발전해가는 TypeScript, 아주 바람직합니다. 👍

image


References