오늘부터 타입스크립트 5.8 베타 버전이 출시되었음을 기쁜 마음으로 알려드립니다.
베타 버전을 사용하시려면 아래 커맨드를 사용하여 npm으로 다운로드 받을 수 있습니다.
npm install -D typescript@beta
타입스크립트 5.8에서는 어떤 새로운 기능이 있는지 같이 살펴보시죠!
/**
* @param prompt 사용자에게 보이는 텍스트
* @param selectionKind 사용자가 선택할 수 있는 개수를 나타내는 유형
* @param items 사용자에게 보이는 각 옵션
**/
async function showQuickPick(
prompt: string,
selectionKind: SelectionKind,
items: readonly string[]
): Promise<string | string[]> {
// ...
}
enum SelectionKind {
Single,
Multiple,
}
showQuickPick
은 하나 이상의 선택 가능한 옵션의 UI 요소를 표시하는 함수입니다. 이 동작은 selectionKind
매개변수에 의해 결정됩니다. selectionKind
가 SelectionKind.Single
일 때 showQuickPick
의 반환 타입은 string
이고, SelectionKind.Multiple
일 때는 string[]
이어야 합니다.
문제는 showQuickPick
의 타입 시그니처가 이 점을 명확히 해주지 않는다는 것입니다. 타입 시그니처는 반환 값이 string | string[]
라고만 명시되어 있어서, 호출자는 이를 명시적으로 확인해야 합니다. 예를 들어, 아래의 예시에서는 shoppingList
가 string[]
타입일 것이라고 예상되지만, 실제로는 더 넓은 string | string[]
타입을 얻게 됩니다.
let shoppingList = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"]
);
console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`);
// ~~~~
// 에러!
// 프로퍼티 'join'은 'string | string[]' 타입에 존재하지 않습니다.
// 프로퍼티 'join'은 'string' 타입에 존재하지 않습니다.
대신 조건부 타입을 사용하여 showQuickPick
의 반환 타입을 더 정확하게 만들 수 있습니다.
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: string;
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[]
): Promise<QuickPickReturn<S>> {
// ...
}
이 방법은 호출자에게 잘 동작합니다!
// `SelectionKind.Multiple`이면 `string[]` - 동작함 ✅
let shoppingList: string[] = await showQuickPick(
"Which fruits do you want to purchase?",
SelectionKind.Multiple,
["apples", "oranges", "bananas", "durian"]
);
// `SelectionKind.Single`이면 `string` - 동작함 ✅
let dinner: string = await showQuickPick(
"What's for dinner tonight?",
SelectionKind.Single,
["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"]
);
하지만 실제로 showQuickPick
을 구현해 보면 어떨까요?
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[]
): Promise<QuickPickReturn<S>> {
if (items.length < 1) {
throw new Error("At least one item must be provided.");
}
// 모든 옵션에 대해 버튼 생성
let buttons = items.map((item) => ({
selected: false,
text: item,
}));
// 필요한 경우 첫 번째 요소를 기본값으로 설정
if (selectionKind === SelectionKind.Single) {
buttons[0].selected = true;
}
// 이벤트 핸들링 코드...
// 선택된 옵션들 찾기
const selectedItems = buttons
.filter((button) => button.selected)
.map((button) => button.text);
if (selectionKind === SelectionKind.Single) {
// 선택된 (유일한) 첫 번째 옵션 고르기
return selectedItems[0];
} else {
// 선택된 모든 옵션들 반환
return selectedItems;
}
}
안타깝게도 타입스크립트는 반환문에서 아래와 같이 에러를 발생시킵니다.
Type 'string[]' is not assignable to type 'QuickPickReturn<S>'.
Type 'string' is not assignable to type 'QuickPickReturn<S>'.
이전까지 타입스크립트에서 고차 조건부 타입을 반환하는 함수를 구현하려면 타입 단언(type assertion)이 필요했습니다.
if (selectionKind === SelectionKind.Single) {
// Pick the first (only) selected item.
- return selectedItems[0];
+ return selectedItems[0] as QuickPickReturn<S>;
}
else {
// Return all selected items.
- return selectedItems;
+ return selectedItems as QuickPickReturn<S>;
}
이는 이상적인 방식이 아닙니다. 타입 단언은 타입스크립트가 수행할 수 있는 적절한 검사를 무력화하기 때문입니다. 예를 들어, 아래 코드처럼 if
/else
분기를 잘못 섞어 놓은 버그를 타입스크립트가 감지하는 것이 더 좋겠죠.
if (selectionKind === SelectionKind.Single) {
// 이런! 호출자는 하나의 항목만을 기대하지만 배열을 반환합니다!
return selectedItems;
} else {
// 앗! 호출자는 배열을 기대하지만 하나의 항목을 반환합니다!
return selectedItems[0];
}
타입 단언을 피하고자, 타입스크립트 5.8에서는 반환문에서 조건부 타입을 제한적으로 검사하는 기능을 추가했습니다. 함수의 반환 타입이 제네릭 조건부 타입일 경우, 타입스크립트는 이제 조건부 타입에서 사용된 제네릭 매개변수에 대해 제어 흐름 분석을 수행하고, 각 매개변수의 좁혀진(narrowed) 타입을 사용하여 조건부 타입을 인스턴스화한 후, 이를 새로운 타입과 비교합니다.
이 기능은 실제로 어떤 의미를 가질까요? 먼저, 어떤 종류의 조건부 타입이 타입 좁히기를 유도하는지 살펴보겠습니다. 표현식에서 타입 좁히기가 작동하는 방식을 반영하려면, 우리는 각 분기에서 어떤 일이 발생하는지를 더욱 명확하고 철저하게 정의해야 합니다.
type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple
? string[]
: S extends SelectionKind.Single
? string
: never;
이렇게 작성하면 예제 코드가 제대로 작동합니다. 호출하는 쪽에서도 아무런 문제가 없으며 타입 안정성을 갖추어 구현할 수 있습니다! 또한 if
분기 내 내용을 바꾸려고 하면 타입스크립트가 올바르게 오류를 감지합니다!
if (selectionKind === SelectionKind.Single) {
// 이런! 호출자는 하나의 항목만을 기대하지만 배열을 반환합니다!
return selectedItems;
// ~~~~~~
// 에러! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
} else {
// 앗! 호출자는 배열을 기대하지만 하나의 항목을 반환합니다!
return selectedItems[0];
// ~~~~~~
// error! 'string[]' 타입을 'string' 타입에 할당할 수 없습니다.
}
이제 타입스크립트는 인덱싱된 액세스 타입을 사용할 때도 비슷한 동작을 수행합니다!
조건부 타입 대신 SelectionKind
값을 원하는 반환 타입으로 매핑하는 타입을 사용할 수도 있습니다.
interface QuickPickReturn {
[SelectionKind.Single]: string;
[SelectionKind.Multiple]: string[];
}
async function showQuickPick<S extends SelectionKind>(
prompt: string,
selectionKind: S,
items: readonly string[]
): Promise<QuickPickReturn[S]> {
// ...
}
많은 사용자에게는 동일한 코드를 더 편리하게 작성하는 방법이 될 것입니다.
이 분석에 대한 자세한 내용은 링크된 제안서와 구현을 참고하세요!
이 기능에는 몇 가지 제한 사항이 있습니다. 이 특별한 검사는 하나의 매개변수가 조건부 타입에서 비교되는 유형과 연관되거나 인덱싱된 액세스 타입에서 키로 사용되는 경우에만 작동합니다. 조건부 타입을 사용할 경우, 최소한 두 개 이상의 타입 체크가 있어야 하며, 마지막 분기는 never
를 포함해야 합니다. 또한 해당 매개변수의 타입은 제네릭 타입이어야 하며 제약 조건으로 유니온 타입이 있어야 합니다. 즉, 다음과 같은 패턴에 대해 코드 분석이 동작합니다.
function f<T extends A | B>(
x: T
): T extends A ? string : T extends B ? number : never;
즉, 특정 프로퍼티가 타입 매개변수와 연관된 경우에는 이러한 검사가 발생하지 않습니다. 예를 들어, 위의 코드를 개별 매개변수 대신 옵션 객체(options bag)로 바꾸면 타입스크립트는 새로운 타입 검사를 하지 않을 겁니다.
interface QuickPickOptions<S> {
prompt: string;
selectionKind: S;
items: readonly string[];
}
async function showQuickPick<S extends SelectionKind>(
options: QuickPickOptions<S>
): Promise<QuickPickReturn<S>> {
// 여기서는 타입 좁히기가 잘 동작하지 않습니다...
}
그러나 내부 내용을 확인하는 조건부 타입을 작성하여 해결할 수 있습니다.
type QuickPickReturn<O extends QuickPickOptionsBase> =
O extends QuickPickOptionsMultiple
? string[]
: O extends QuickPickOptionsSingle
? string
: never;
interface QuickPickOptionsBase {
prompt: string;
items: readonly string[];
}
interface QuickPickOptionsSingle extends QuickPickOptionsBase {
selectionKind: SelectionKind.Single;
}
interface QuickPickOptionsMultiple extends QuickPickOptionsBase {
selectionKind: SelectionKind.Multiple;
}
async function showQuickPick<
Opts extends QuickPickOptionsSingle | QuickPickOptionsMultiple
>(options: Opts): Promise<QuickPickReturn<Opts>>;
이러한 규칙들이 다소 복잡해 보일 수 있지만, 실제로는 대부분의 코드에서 이러한 고차 타입을 활용할 필요가 없습니다.
타입 단언보다 이번에 추가된 새로운 검사 메커니즘을 권장하기는 합니다. 하지만 가능하다면 API를 단순하게 유지하는 것이 타입을 더 쉽게 작성하는 데 도움이 되므로, 이를 우선으로 고려하시길 바랍니다.
한편, 우리는 코드 작성이 쉬워지도록 하면서도 이러한 제한을 완화할 방법을 계속 찾을 것입니다.
--module nodenext
의 ECMAScript 모듈 require()
지원수년 동안, Node.js는 ECMAScript 모듈(ESM)과 CommonJS 모듈을 함께 지원했습니다. 불행히도 두 모듈 간의 상호 운용성에는 몇 가지 어려움이 있었습니다.
import
할 수 있음require()
할 수 없음즉, ESM 파일에서 CommonJS 파일을 사용하는 것은 가능했지만 그 반대는 불가능했습니다. 이로 인해 ESM 지원을 제공하려는 라이브러리 개발자들은 여러 도전 과제에 직면했습니다. 라이브러리 개발자는 CommonJS 사용자와의 호환성을 포기하거나, "이중 배포"를 해야 했습니다(ESM과 CommonJS를 위한 별도의 진입점을 제공). 또는 그냥 CommonJS에 계속 머무르기도 했습니다. 이중 배포는 좋은 절충안처럼 보일 수 있지만, 복잡하고 오류가 발생하기 쉬울뿐더러 패키지 코드 양을 대략 두 배로 늘리는 결과를 초래합니다.
Node.js 22는 이러한 제약 사항 중 일부를 완화하여 CommonJS 모듈에서 ESM 모듈을 require("esm")
호출로 사용할 수 있도록 합니다. Node.js는 여전히 최상위 await
이 포함된 ESM 파일에 대해 require()
를 허용하지 않지만, 대부분의 다른 ESM 파일은 이제 CommonJS 파일에서 사용할 수 있습니다. 이는 라이브러리 개발자들이 이중 배포를 하지 않고도 ESM 지원을 제공하는 중요한 기회를 제공합니다.
타입스크립트 5.8은 --module nodenext
플래그에서 이러한 동작을 지원합니다. --module nodenext
가 활성화되면, ESM 파일에 대한 require()
호출에서 타입 에러를 발생시키지 않습니다.
이 기능은 이전 버전의 Node.js로도 백포트(backport)될 수 있기 때문에, 현재 이 동작을 활성화하는 안정적인 --module nodeXXXX
옵션은 없습니다. 그러나 향후 버전의 타입스크립트는 이 기능을 node20
에서 안정화할 수 있을 것으로 예상됩니다. 그동안 Node.js 22 이상을 사용하는 사용자들은 --module nodenext
를 사용하는 것을 권장하며, 구버전 Node.js 사용자는 --module node16
(또는 --module node18으로의 소규모 업데이트)을 유지해야 합니다.
자세한 내용은 require("esm")에 대한 지원을 확인해 보세요.
--module node18
타입스크립트 5.8에는 안정적인 --module node18
플래그가 도입되었습니다. Node.js 18을 고정으로 사용할 때 이 플래그는 --module nodenext
에 있는 특정 동작을 포함하지 않는 안정적인 참조 지점을 제공합니다. 구체적으로 설명하면 다음과 같습니다.
require()
는 node18
에서는 허용되지 않지만 nodenext
에서는 허용됨node18
에서는 허용되지만 nodenext
에서는 허용되지 않음자세한 내용은 --module node18
풀 리퀘스트와 --module nodenext
에 적용된 변경 사항을 모두 참고하세요.
--erasableSyntaxOnly
옵션최근에 Node.js 23.6은 타입스크립트 파일을 직접 실행하는 실험적 지원을 정식으로 도입했습니다. 그러나 이 모드에서는 일부 특정 구문만 지원됩니다. Node.js는 --experimental-strip-types
모드를 정식으로 활성화했는데, 이 모드는 타입스크립트 전용 구문이 런타임에서 의미를 가지지 않도록 요구합니다. 다시 말해, 타입스크립트 전용 구문을 쉽게 "제거"하여 유효한 자바스크립트 파일을 남길 수 있다는 것입니다.
즉, 다음과 같은 기능은 사용할 수 없습니다.
enum
선언namespaces
와 modules
import
별칭(alias)ts-blank-space 또는 Amaro(Node.js의 타입 제거를 위한 기본 라이브러리)와 같은 유사한 도구에도 동일한 제한이 있습니다. 이러한 도구는 요구 사항을 충족하지 않는 코드가 발견되면 유용한 에러 메시지를 제공하지만 실제로 코드를 실행하기 전까지는 코드가 작동하지 않는다는 사실을 알 수 없습니다.
이것이 바로 타입스크립트 5.8에 --erasableSyntaxOnly
플래그가 도입된 이유입니다. 이 플래그를 활성화하면 타입스크립트는 지울 수 있는 타입 구문만 사용하도록 허용하고 지울 수 없는 구문을 발견하면 에러를 발생시킵니다.
class C {
constructor(public x: number) {}
// ~~~~~~~~~~~~~~~~
// 에러! 이 문법은 'erasableSyntaxOnly'가 활성화되었을 때 허용되지 않습니다.
}
더 자세한 정보는 구현 내용을 참고하세요.
--libReplacement
플래그TypeScript 4.5에서는 기본 lib
파일을 사용자 정의 라이브러리 파일로 대체할 수 있는 기능을 도입했습니다. 이 기능은 @typescript/lib-\*
패키지에서 라이브러리 파일을 해결할 수 있는 가능성에 기반했습니다. 예를 들어, 다음과 같은 package.json
을 사용하여 dom
라이브러리를 특정 버전의 @types/web
패키지로 고정할 수 있었습니다.
{
"devDependencies": {
"@typescript/lib-dom": "npm:@types/web@0.0.199"
}
}
위와 같이 설치되면, @typescript/lib-dom
이라는 패키지가 존재해야 하며, 타입스크립트는 현재 설정에 dom
이 암시되어 있다면 항상 이를 조회할 것입니다.
이러한 타입 고정은 강력하지만 약간의 추가 작업을 초래합니다. 해당 기능을 사용하지 않더라도 타입스크립트는 항상 조회 작업을 수행하며, lib
의 대체 패키지가 존재하기 시작할 경우를 대비해 node_modules의 변화를 감시해야 합니다.
타입스크립트 5.8에서는 --libReplacement
플래그를 도입하여 이 동작을 비활성화할 수 있게 되었습니다. --libReplacement
를 사용하지 않으면 이제 --libReplacement false
로 비활성화합니다. 앞으로 --libReplacement false
가 기본값이 될 수 있으므로, 현재 이 동작에 의존하고 있다면 이를 명시적으로 활성화하기 위해 --libReplacement true
를 설정하는 것을 고려해야 합니다.
자세한 내용은 이 변경사항을 참고하세요.
계산된 프로퍼티가 선언 파일에서 더 예측 가능한 방식으로 생성되도록 하기 위해, 타입스크립트 5.8은 클래스에서 계산된 프로퍼티 이름에 있는 엔터티 이름(bareVariables
및 dotted.names.that.look.like.this
)을 일관되게 유지할 것입니다.
예를 들어, 다음과 같은 코드를 생각해 보세요.
export let propName = "theAnswer";
export class MyClass {
[propName] = 42;
// ~~~~~~~~~~
// 에러!
// 클래스 프로퍼티 선언에서 계산된 프로퍼티 이름은 단순한 리터럴 타입이거나 'unique symbol' 타입이어야 합니다.
}
이전 버전의 타입스크립트는 이 모듈에 대한 선언 파일을 생성할 때 오류가 발생하고 최선의 노력으로 생성된 선언 파일은 인덱스 시그니처를 생성합니다.
export declare let propName: string;
export declare class MyClass {
[x: string]: number;
}
타입스크립트 5.8에서는 이제 예제 코드가 허용되며 내보낸 선언 파일은 작성한 내용과 일치합니다.
export declare let propName: string;
export declare class MyClass {
[propName]: number;
}
이렇게 하면 클래스에 정적으로 이름이 지정된 프로퍼티가 생성되지 않는다는 점에 유의하세요. 여전히 [x: string]: number
처럼 사실상 인덱스 시그니처를 갖게 되므로 unique symbols
나 리터럴 타입을 사용해야 합니다.
이 코드를 작성하면 --isolatedDeclarations
플래그에서 오류가 발생했지만, 이번 변경으로 인해 일반적으로 선언문에서 계산된 프로퍼티 이름이 허용될 것으로 예상됩니다.
가능성은 낮지만 타입스크립트 5.8로 컴파일된 파일이 타입스크립트 5.7 이하에서 하위 호환되지 않는 선언 파일을 생성할 수 있습니다.
자세한 내용은 구현 PR을 참조하세요.
타입스크립트 5.8은 프로그램을 빌드하는 시간을 개선했으며, --watch
모드나 편집기에서 파일 변경에 따라 프로그램을 업데이트할 수 있는 여러 가지 최적화를 도입했습니다.
먼저, 타입스크립트는 이제 경로를 정규화하는 과정에서 발생할 수 있는 배열 할당을 지양합니다. 일반적으로 경로 정규화는 경로의 각 부분을 문자열 배열로 분할하고, 결과적인 경로를 상대적인 부분을 기준으로 정규화한 후, 이를 표준 구분자를 사용하여 다시 결합하는 과정입니다. 많은 파일이 있는 프로젝트에서는 이 작업이 상당히 반복적이고 시간이 오래 걸릴 수 있습니다. 타입스크립트는 이제 배열 할당을 피하고, 원래 경로의 인덱스에서 더 직접적으로 작업을 수행합니다.
또한, 프로젝트의 기본 구조를 변경하지 않는 편집이 이루어졌을 때, 타입스크립트는 이제 제공된 옵션들(예: tsconfig.json
의 내용)을 다시 검증하지 않습니다. 예를 들어, 간단한 편집이 프로젝트의 출력 경로가 입력 경로와 충돌하는지 확인하지 않고 마지막 검사 결과를 사용할 수 있습니다. 이는 대규모 프로젝트에서 속도가 더 빠르고 반응성이 좋게 느껴질 것입니다.
이 섹션에서는 업그레이드의 일환으로 주목할 만한 변경 사항을 강조합니다. 때로는 더 이상 사용되지 않는 기능, 제거된 기능, 새로운 제한 사항들을 다룰 수 있습니다. 또한 기능적으로 개선된 버그 수정이 포함될 수 있으며, 이러한 수정은 기존 빌드에 새로운 에러를 발생시킬 수 있습니다.
lib.d.ts
DOM에 대해 생성된 타입은 코드베이스의 타입 검사에 영향을 미칠 수 있습니다. 자세한 내용은 이 버전의 타입스크립트애서 DOM 및 lib.d.ts
업데이트와 관련된 링크된 이슈를 참조하세요.
--module nodenext
임포트 단언에 대한 제한 사항// 임포트 단언 ❌ - 대부분의 런타임과 향후 호환되지 않음
import data from "./data.json" assert { type: "json" };
// 임포트 속성 ✅ - JSON 파일을 가져올 때 권장되는 방식
import data from "./data.json" with { type: "json" };
import data from "./data.json" assert { type: "json" };
// ~~~~~~
// 에러! 임포트 단언은 임포트 속성에 의해 대체되었습니다. 'assert' 대신 'with'을 사용하세요.
더 자세한 정보는 변경사항을 참고하세요.
현재 타입스크립트 5.8은 "기능 안정화" 상태입니다. 타입스크립트 5.8의 주요 초점은 버그 수정, 다듬기 작업, 그리고 몇 가지 위험도가 낮은 IDE 기능입니다. 몇 주 후에는 릴리스 후보가 제공될 예정이며, 그 후에는 안정적인 버전이 곧 출시될 것입니다. 출시 계획에 맞춰 준비하고자 한다면, 목표 릴리스 날짜와 더 많은 정보를 담고 있는 우리의 개발 계획을 꼭 확인하세요.
참고로 베타 버전은 다음 버전을 시험해 볼 좋은 방법이지만, 릴리스 후보가 나오기 전까지는 개발자 버전 빌드를 통해 타입스크립트 5.8의 최신 버전을 체험할 수 있습니다. 우리의 개발자 버전 빌드는 많은 테스트를 거쳤으며, 로컬 IDE에서만 사용해도 문제가 없습니다.
그러니 오늘 베타 또는 개발자 버전을 사용해 보시고 여러분의 의견을 들려주세요!
행복한 코딩하세요!
– Daniel Rosenwasser 및 타입스크립트 팀으로부터