타입스크립트 🆃🆂 4.1 별거 없다⚔️

타입스크립트 4.1 새로운 기능들과 변경 사항들

MJ Studio
MJ Studio
14 min readDec 15, 2020

--

어느날 타입스크립트가 업데이트 되었나 보니 제가 쓰고있던 4.0.5 에서 4.1.3 로 급격한 업데이트가 발생했음을 알 수 있었습니다(사실 제가 별 관심 없었습니다). 이와 관련해서 4.1 버전의 새로운 타입스크립트 언어 기능들과 변경점들을 살펴보는 시간을 가지려 합니다.

4.0 버전의 변경사항이 궁금하신 분들은 다음과 같은 제 예전 글을 참고해주세요.

템플릿 리터럴 타입(Template Literal Types)

흔히 리터럴이라면 특정 자료형을 raw 하게 표현할 수있는 언어의 기능을 의미하는데, 이게 이제 타입에도 적용이 됩니다. 다음과 같이 ES6의 문자열 리터럴 문법과 동일합니다.

type Height = '170' | '180';
type Weight = '50' | '60' | '70';

type BodyProfile = `${Height}cm-${Weight}kg`;

const myBody: BodyProfile = '170cm-50kg';

카르테시안 곱으로 총 6가지 조합의 문자열 enum 타입이 BodyProfile 이라는 이름으로 만들어지게 됩니다. ‘170cm-20kg’ 등 존재할 수 없는 조합으로 값이 입력되면 TS 에러가 나게 됩니다.

조금 복잡한 예시를 살펴보겠습니다.

let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});

person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});

makeWatchedObject 는 observable 한 객체를 만들어주는 함수라고 생각하겠습니다. 그리고 우리는 반환된 객체에 on 함수를 이용해 특정 객체의 필드가 변경되었을 때 이벤트를 전달받을 수 있습니다.

여기서 우리가 on 의 첫 인자로 들어갈 이벤트명을 makeWatchedObject 에 전달된 객체의 키들 + Changed 로 제한해주고싶다면 어떻게 해야할까요? firstNameChangedageChanged 로 말입니다.

다음과 같이 해주면 됩니다.

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

/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): PropEventSource<T>;

makeWatchedObject 로 전달된 객체의 타입이 T 형식 인자가 되어 `${string & keyof T}Changed` 라는 템플릿 리터럴 타입으로 지정되었습니다. 이제 우리는 객체의 키 + Changed 가 아닌 문자열로 on 메소드를 호출하려 할 때 에러가 발생할 것입니다. 실제로 보시죠.

firstNameChangedageChanged가 아닌 secondNameChanged를 전달하니 바로 에러가 발생하고 가능한 타입들도 모두 보여줍니다.

새로운 타입 도우미들(New Type Aliases)

Partial, Omit, Exclude, Record, Pick 등 타입들을 쉽게 지정할 수 있는 도우미타입들이 있었습니다만, 새로운 문자열 관련 도우미타입이 4개가 추가되었고 모두 대소문자 변환과 관련되어 있습니다.

/**
* Convert string literal type to uppercase
*/
type Uppercase<S extends string> = intrinsic;

/**
* Convert string literal type to lowercase
*/
type Lowercase<S extends string> = intrinsic;

/**
* Convert first character of string literal type to uppercase
*/
type Capitalize<S extends string> = intrinsic;

/**
* Convert first character of string literal type to lowercase
*/
type Uncapitalize<S extends string> = intrinsic;

위와 같이 첫 글자를 소문자로 만드는 문자열 타입을 만들 수도 있습니다.

매핑된 타입에서 키 재매핑(Key Remapping in Mapped Types)

Mapped types란 다음과 같습니다.

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

보통 위와 같이 전달된 타입에 대해 대괄호 문법을 이용하여 1:1 매핑을 시킵니다. 그러면

type Person = {
name: string;
age: string;
};

type ReadonlyPerson = Readonly<Person>;

ReadonlyPersonPerson의 모든 필드들이 readonly 가 됩니다.

타입스크립트 4.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>;
/*
* interfcae LazyPerson{
* getName(): string;
* getAge(): number;
* location(): string;
* }

* */

Person 이란 타입이 LazyPerson 이 되었는지 아시겠나요? 우선, K in keyof T 에서 KT의 키들을 의미하니까 Person의 키인 name, age, location 들이 됩니다.

그 후에 `as get${Capitalize<string & K>}` 가 적용되는데, 이는 앞에 get을 붙이고 첫 글자를 대문자로 만든 템플릿 리터럴 형식을 새로운 타입 도우미(Capitalize)와 함께 사용한 것입니다. 이렇게 키들을 매핑시키고 값들은 인덱스된 접근 연산자(Indexed access operator) T[K] 를 반환하는 함수 타입이 되었습니다.

참고로 인덱스된 접근 연산자(Indexed access operator)는 제네릭 형식에서 흔히 사용되는 문법인데, 예를 들어 Person[‘name’]string을 의미하게 된다는 타입스크립트의 reflection 방법입니다. 즉, name이 매핑되고 있을 때 T[K]Person[‘name’] 이므로 string 타입이 된다는 것이죠.

재귀적 조건 타입(Recursive Conditional Types)

타입스크립트 4.1부터 조건 타입을 사용해서 재귀적으로 타입을 지정할 수 있습니다.

type ElementType<T> =
T extends ReadonlyArray<infer U> ? ElementType<U> : T;

function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
throw "not implemented";
}

// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

타입스크립트에서 조건 타입은 제네릭에서 extends 를 사용하여 삼항 연산자처럼 타입을 지정하는 것인데, 이제 여기에 자기 자신의 타입을 넣어서 재귀적인 구조를 띄는 타입을 만들 수 있는 것입니다.

위의 예시에서 deepFlatten 을 호출한 예시 3줄은 모두 number[] 을 반환하게 됩니다.

검사되는 인덱스 접근(Checked Indexed Accesses, —noUncheckedIndexedAccess)

다음과 같은 타입이 있습니다.

interface Options {
path: string;
permissions: number;

// Extra properties are caught by this index signature.
[propName: string]: string | number;
}

Options 이라는 타입에는 pathpermissions는 무조건 존재하는 타입이라고 가정되고 그 이외에 어떠한 키에 대한 접근은 그 타입이 string 이나 number 라고 암시적으로 가정됩니다.

이제 noUncheckedIndexedAccess 옵션을 사용하면 이에 대해 강제적으로 undefined 일 가능성을 추가할 수 있습니다.

즉 다른 타입들이 string | number 가 아닌 string | number | undefined 가 된다는 것이죠.

이는 배열에서도 마찬가지입니다. i 번째 인덱스가 실제로 있는 녀석인지 판단할 수 없기 때문에 strs[i]undefined가 될 수도 있다는 에러가 발생합니다. 이에 대한 해결책은 for-offorEach 입니다.

function screamLines(strs: string[]) {
// this will have issues
for (let i = 0; i < strs.length; i++) {
console.log(strs[i].toUpperCase());
// ~~~~~~~
// error! Object is possibly 'undefined'.
}
}

React 17 JSX Factories

리액트 17은 새로운 기능들은 추가되지 않았지만 새로운 JSX transformation 방식이 채택되었습니다.

사실 JSX 문법은 React.createElement 와 같은 React 의 API로 바벨같은 트랜스파일러에 의해 transformation 되는데, 이제 새로운 JSX transformer를 사용하게 되었습니다. 물론 바벨이 안쓰이는 것은 아닙니다.

가장 눈에띄는 개선은 이는 우리가 더이상 JSX 코드를 작성하기 위해 import React from ‘react’; 를 모듈 최상단에 적지 않아도 된다는 것입니다.

이것을 타입스크립트에서 지원하기 위해 타입스크립트 4.1은 새로운 jsx 컴파일러 옵션을 지원합니다.

  • react-jsx
  • react-jsxdev
// 실제 환경의 ts.config ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}
// 개발 환경의 ts.config ./src/tsconfig.dev.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev"
}
}

그 외의 변화들

paths without baseUrl

이제 paths 옵션을 baseUrl 옵션 없이 사용할 수 있습니다.

checkJs Implies allowJs

이제 checkJs 옵션만 사용해도 allowJs 옵션이 켜집니다.

Editor Support for the JSDoc @see Tag

에디터가 @see 태그로 작성된 JSDoc 을 지원합니다.

lib.d.ts Changes

abstract Members Can’t Be Marked async

any/unknown Are Propagated in Falsy Positions

any/unknown 타입의 변수는 논리연산자와 함께 쓰일 때 truthy 한 값으로 사용되어왔습니다.

declare let foo: unknown;
declare let somethingElse: { someProp: string };

let x = foo && somethingElse;

xsomethingElse의 타입이 되어왔다는 것입니다.

그러나 타입스크립트 4.1 부터는 foo 의 타입이 x 의 타입(unknown or any)이 됩니다.

resolve’s Parameters Are No Longer Optional in Promises

Promise 를 직접 만들 때 resolve 의 인자가 더이상 optional이 아닙니다. resolve() 와같이 호출을 할 수 없습니다. 그러나 정말 resolve 가 인자없이 호출되야 된다면 Promise<void> 타입을 사용하면 됩니다.

new Promise<void>(resolve => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
})
})

Conditional Spreads Create Optional Properties

interface Animal {
name: string;
}
function copyOwner(pet?: Animal) {
return {
...(pet?.owner),
otherStuff: 123
}
}

copyOwner의 반환형은 4.1 전까지는 다음과 같은 union 타입이 되어왔습니다.

{ otherStuff: number } | { otherSutff: number, name: string }

그러나 이제 다음과 같이 결합된 형태로 옵셔널이 추가되어집니다.

{ otherStuff: number, name?: string }

글을 마치며

간단한 예시들과 함께 타입스크립트 4.1의 새롭게 추가된 문법들이나 기능들을 살펴보았습니다. 이 모든걸 알 필요는 없지만 적재적소에 쓴다면 더 fancy한 타입스크립트 개발자가 될 수 있을 것입니다. 이보다 더 중요한건 타입스크립트 핸드북에 있는 기본 문법이라는 것도 명심하시길 바랍니다.

읽어주셔서 감사합니다 😀

--

--