타입스크립트 5.1이 나온지 꽤 된 시점이다.
여태 타입스크립트를 사용하지 않고 있다가 올해 3월 처음 타입스크립트를 회사 프로젝트에 적용하며 쭉 사용해왔었다.
그러면서 타입스크립트 공식문서를 몇 번 방문하게 되었고 이번 업데이트 버전인 5.1버전에 대해 읽고 글을 쓰게 되었다.
- 이 글은 타입스크립트 공식문서의 글을 번역한 글입니다.
undefined 반환 함수에 대한 더 쉬운 암시적 변환
자바스크립트에서는 함수의 return값을 정해주지 않는다면 undefined값을 반환합니다.
function foo() {
// no return
}
const x = foo();
console.log(x); // undefined
그러나 이전 버전의 Typescript에서는 return 문이 전혀 없을 수 있는 유일한 함수는 void와 any였습니다.
이는 즉, "함수는 undefined를 반환한다"고 명시적으로 선언하더라도 최소한 하나의 반환문을 가지도록 강제되었다는 것을 의미합니다.
// ✅ fine - we inferred that 'f1' returns 'void'
function f1() {
// no returns
}
// ✅ fine - 'void' doesn't need a return statement
function f2(): void {
// no returns
}
// ✅ fine - 'any' doesn't need a return statement
function f3(): any {
// no returns
}
// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
function f4(): undefined {
// no returns
}
만약 어떤 API가 undefined를 반환하는 함수를 요구한다면, 이는 상당한 고통을 초래할 수 있습니다.
최소한 하나의 명시적인 undefined 반환 또는 반환문과 명시적인 주석이 필요하기 때문입니다.
declare function takesFunction(f: () => undefined): undefined;
// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
// no returns
});
// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
takesFunction((): undefined => {
// no returns
});
// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(() => {
return;
});
// ✅ works
takesFunction(() => {
return undefined;
});
// ✅ works
takesFunction((): undefined => {
return;
});
이러한 동작은 개발자이 통제를 벗어난 함수를 호출할 때 혼란스럽고 답답한 상황이었습니다. void와 undefined 간의 상호작용, undefined를 반환하는 함수가 변환문을 필요로 하는지 여부 등을 이해시키는 것은 주의를 분산시키는 요소로 느껴집니다.
첫째로 Typescript 5.1에서는 이제 undefined를 반환하는 함수가 반환문이 없어도 허용됩니다.
// ✅ Works in TypeScript 5.1!
function f4(): undefined {
// no returns
}
// ✅ Works in TypeScript 5.1!
takesFunction((): undefined => {
// no returns
});
둘째로, 만약 함수에 반환 표현식이 없고, 해당 함수가 udnefined를 반환하는 함수를 요구하는 곳에 전달된다면, Typescript는 해당 함수의 반환 타입을 undefined로 추론합니다.
// ✅ Works in TypeScript 5.1!
takesFunction(function f() {
// ^ return type is undefined
// no returns
});
// ✅ Works in TypeScript 5.1!
takesFunction(function f() {
// ^ return type is undefined
return;
});
또 다른 유사한 문제를 해결하기 위해, Typescript의 --nolmplicitReturns옵션에서, undefined만 반환하는 함수들은 이제 void와 비슷한 예외 규칙을 갖습니다. 즉, 모든 코드 경로가 명시적인 반환문으로 끝나지 않아도 됩니다.
Getter, Setter
Typescript 4.3부터는 getter와 setter 접근자의 경우 서로 관련 없는 타입을 지정할 수 있게 되었습니다.
interface Serializer {
set value(v: string | number | boolean);
get value(): string;
}
declare let box: Serializer;
// Allows writing a 'boolean'
box.value = true;
// Comes out as a 'string'
console.log(box.value.toUpperCase());
초기에는 get 타입이 set 타입의 하위 타입이어야 했습니다. 이는 즉, get 접근자의 타입이 set접근자의 타입을 포함해야 한다는 의미입니다.
box.value = box.value;
그러나 기존 제안된 많은 API에서는 getter와 setter 간에 전혀 관련없는 타입을 가질 수 있습니다. 예를들어, DOM 및 VSSStyleRule API에서 가장 일반적인 예인 style 속성을 살펴보겠습니다. 모든 스타일 규칙은 CSSStyleDeclaration 타입인 style 속성을 가지고 있지만, 이 속성에 값을 작성하려고 할 때는 문자열만 올바르게 작성할 수 있습니다.
Typescript5.1 버전부터는 명시적인 타입 주석이 있는 경우에 한하여 getter와 setter 접근자 속성에 전혀 관련없는 타입을 허용합니다.
또한, 이 버전의 Typescript에서는 이러한 내장 인터페이스의 타입을 변경하지는 않지만 CSSStyleRule은 이제 다음과 같이 정의할 수 있습니다.
interface CSSStyleRule {
// ...
/** Always reads as a `CSSStyleDeclaration` */
get style(): CSSStyleDeclaration;
/** Can only write a `string` here. */
set style(newValue: string);
// ...
}
이러한 변경으로 인해, set 접근자가 "유효한" 데이터만 허용하도록 욕하고, get 접근자가 어떤 기저 상태가 아직 초기화되지 않았을 경우 undefined를 반환할 수 있는 등 다른 패턴도 가능해졌습니다.
JSX요소와 JSX태그 타입 간의 결합도가 낮아진 타입체크
JSX에서 Ttypescript가 직면한 문제중 하나는 모든 JSX요소의 태그에 대한 타입 요구사항이었습니다.
// A self-closing JSX tag
<Foo />
// A regular element with an opening/closing tag
<Bar></Bar>
Typescript에서 <Foo/> or <Bar></Bar>와 같은 JSX 요소를 타입 체크할 때 항상 JSX라는 네임스페이스를 찾고 거기서 JSX.Element라는 타입을 가져옵니다.
다시말해, Typescript에서 JSX.Element에 호환성을 확인하기 위해 Foo 또는 Bar에서 반환되거나 구성된 타입을 가져와 확인합니다.
하지만 Typescript에서는 Foo또는 Bar 자체가 태그 이름으로 사용될 수 있는지 유요한 여부를 확인하기 위해 해당 컴포넌트가 반환되거나 구성된 타입을 가져와서 JSX.Element와의 호환성을 확인하는 것에 제한이 있었습니다. 예를들어, JSX라이브러리에서는 컴포넌트가 문자열이나 프로미스를 반환하는 것도 허용할 수 있습니다.
더 구체적인 예로, React는 프로미스를 반환하는 컴포넌트에 대한 제한적인 지원을 추가하는 것을 고려중입니다. 그러나 기존 버전의 Typescript에서는 JSX.Element의 타입ㅇ르 크게 허용하지 않고 있으므로 이를 표현할 수 없습니다.
import * as React from "react";
async function Foo() {
return <div></div>;
}
let element = <Foo />;
// ~~~
// 'Foo' cannot be used as a JSX component.
// Its return type 'Promise<Element>' is not a valid JSX element.
라이브러리에서 이를 표현할 수 있도록 하기 위해 Typescript5.1에서는 JSX.ElementType이라는 타입을 조회합니다. ElementType은 JSX 요소에서 태그로 사용할 수 있는 유효한 타입을 정확히 지정합니다. 따라서 현재로서 다음과 같은 형식으로 타입이 지정될 수 있습니다.
namespace JSX {
export type ElementType =
// All the valid lowercase tags
keyof IntrinsicAttributes
// Function components
(props: any) => Element
// Class components
new (props: any) => ElementClass;
export interface IntrinsictAttributes extends /*...*/ {}
export type Element = /*...*/;
export type ClassElement = /*...*/;
}
네임스페이스 JSX속성
Typescript는 이제 JSX를 사용할 때 네임스페이스 속성을 제공합니다.
import * as React from "react";
// Both of these are equivalent:
const x = <Foo a:b="hello" />;
const y = <Foo a : b="hello" />;
interface FooProps {
"a:b": string;
}
function Foo(props: FooProps) {
return <div>{props["a:b"]}</div>;
}
JSX.IntrinsicAttributes 네임스페이스 태그는 이름의 첫 번째 세그먼트가 소문자 이름일 때 유사한 방식으로 조회합니다.
// In some library's code or in an augmentation of that library:
namespace JSX {
interface IntrinsicElements {
["a:b"]: { prop: string };
}
}
// In our code:
let x = <a:b prop="hello!" />;
글을 마치며
어떠한 것이든 편함과 익숙함 무서운 것이다.
이는 개발에서도 마찬가지인데 평소 바닐라 자바스크립트를 사용했다면 바닐라 자바스크립트에 익숙할 것이다.
나는 바닐라 자바스크립트 대신 리액트에 더 익숙하다.
굳이 타입스크립트를 사용하지 않더라도 개발을 하는데 있어 불편함과 타입스크립트를 사용하지 않음으로 발생하는 큰 에러는 없었다.
바닐라 자바스크립트에서 리액트. 리액트에서 리덕스 적용. 그리고 타입스크립트 적용까지.
중간중간 "react-query"와 같이 어떤 라이브러리/프레임워크를 적용시킨 적도 있었지만, 내가 적용을 시키며 큰 어려움을 느낀 것은 위에 언급한 세개이다.
솔직히 말하자면 리액트와 리덕스만을 사용할 때 "타입스크립트를 적용시킬 필요가 있나"라는 의문이 들었다.
그러나 익숙하고 편안하면 고이기 마련이고 고인물은 썩기 마련이다.
난 전세계 상위 1퍼센트의 개발자를 바라지는 않는다.
내 한계를 정확히 알고 내 능력을 알고있다.
나는 적어도 "썩은 개발자"는 되지 않으려한다.
익숙함에 취하고 항상 새로운 무언가를 하는 것에 두려움을 느끼지만 적어도 배움에 대한 열망은 존재한다.
그 열망이 typescript도입으로 이끌었고 지금와서 내가 작성한 코드를 돌아보면 "널체크"등의 사소한 것을 전혀하지 않았다는 것을 알 수 있다.
또한 공식문서를 읽으며 그동안 알지 못했던 타입스크립트의 기술을 알 수 있었다.
이는 분명 좋은 것이다.
익숙함이란 라이브러리 등에 한계를 두지 않고 작은 코드에서부터 느껴진다.
예를들어 반복문으로 특정 값을 필터하는 것을 array.prototype.filter()를 사용한다거나.
단순한 명령형 코드와 선언형 코드지만 이 차이는 나를 더욱 발전시킬 수 있는 계기다.