TypeScript 6.0의 새로운 기능, 변경 사항, 주요 사용 중단 항목, 그리고 TypeScript 7.0으로의 전환 준비 방법을 소개합니다.
오늘 TypeScript 6.0을 사용할 수 있게 되었음을 기쁘게 발표합니다!
TypeScript에 익숙하지 않다면, 이 언어는 타입을 위한 문법을 추가해 JavaScript를 확장한 언어입니다. 이를 통해 타입 검사를 사용해 오류를 잡아내고, 풍부한 편집기 도구도 제공할 수 있습니다. TypeScript와 시작 방법에 대한 자세한 내용은 TypeScript 웹사이트에서 확인할 수 있습니다.
하지만 이미 이 언어에 익숙하다면, 다음 명령으로 npm을 통해 TypeScript 6.0을 받을 수 있습니다.
npm install -D typescript
TypeScript 6.0은 현재 JavaScript 코드베이스를 기반으로 하는 마지막 릴리스가 되도록 의도하고 있다는 점에서 특별한 릴리스입니다. 지난해 발표했듯이 (최근 업데이트는 여기), 저희는 네이티브 코드의 속도와 공유 메모리 멀티스레딩의 이점을 활용하는 Go로 작성된 TypeScript 컴파일러 및 언어 서비스의 새 코드베이스를 작업 중입니다. 이 새 코드베이스는 TypeScript 7.0 이후의 기반이 될 것입니다.
TypeScript 6.0은 TypeScript 5.9와 7.0 사이의 다리 역할을 합니다. 따라서 TypeScript 6.0의 대부분의 변경 사항은 TypeScript 7.0 도입에 맞추고 준비하는 데 목적이 있습니다. 놀랍게 들릴 수도 있지만, TypeScript 7.0은 실제로 거의 완성 단계에 매우 가깝습니다. Visual Studio Code에서 사용해 볼 수 있고, npm에서 설치할 수도 있습니다. 사실 TypeScript 6.0을 도입할 수 있다면, TypeScript 7.0의 네이티브 프리뷰도 함께 사용해 보시길 권장합니다.
그렇다고 해서 TypeScript 6.0의 새 기능과 개선 사항이 모두 정렬을 위한 것만은 아닙니다. 이번 릴리스의 주요 내용을 먼저 살펴본 다음, 7.0에서 무엇이 바뀌는지와 이에 어떻게 대비할지 더 자세히 알아보겠습니다.
TypeScript 6.0 베타 이후로 몇 가지 주목할 만한 변경을 적용했으며, 대부분은 TypeScript 7.0의 동작에 맞추기 위한 것입니다.
그중 하나는 제네릭 호출에서의 함수 식에 대한 타입 검사 조정이며, 특히 제네릭 JSX 식에서 발생하는 경우입니다(이 pull request 참조). 일반적으로 이는 기존 코드에서 더 많은 버그를 잡아내지만, 일부 제네릭 호출에는 명시적 타입 인수가 필요할 수도 있습니다.
또한 import assertion 문법(import ... assert {...})에 대한 사용 중단 범위를 import() 호출인 import(..., { assert: {...}})까지 확장했습니다.
마지막으로, 최신 웹 표준을 반영하도록 DOM 타입을 업데이트했으며, 여기에는 Temporal API에 대한 몇 가지 조정도 포함됩니다.
this가 없는 함수에서의 컨텍스트 민감도 감소매개변수에 명시적 타입이 적혀 있지 않더라도, TypeScript는 보통 기대 타입을 기반으로 하거나 같은 함수 호출 안의 다른 인수를 통해 타입을 추론할 수 있습니다.
declare function callIt<T>(obj: {
produce: (x: number) => T,
consume: (y: T) => void,
}): void;
// Works, no issues.
callIt({
produce: (x: number) => x * 2,
consume: y => y.toFixed(),
});
// Works, no issues even though the order of the properties is flipped.
callIt({
consume: y => y.toFixed(),
produce: (x: number) => x * 2,
});
여기서 TypeScript는 속성 순서와 상관없이 produce 함수에서 추론한 T를 기반으로 consume 함수 안의 y 타입을 추론할 수 있습니다. 그런데 이 함수들이 화살표 함수 문법 대신 메서드 문법 으로 작성되면 어떨까요?
declare function callIt<T>(obj: {
produce: (x: number) => T,
consume: (y: T) => void,
}): void;
// Works fine, `x` is inferred to be a number.
callIt({
produce(x: number) { return x * 2; },
consume(y) { return y.toFixed(); },
});
callIt({
consume(y) { return y.toFixed(); },
// ~
// error: 'y' is of type 'unknown'.
produce(x: number) { return x * 2; },
});
이상하게도 callIt의 두 번째 호출은 오류를 발생시키는데, TypeScript가 consume 메서드의 y 타입을 추론하지 못하기 때문입니다. 여기서 일어나는 일은, TypeScript가 T의 후보를 찾으려고 할 때 먼저 매개변수에 명시적 타입이 없는 함수를 건너뛴다는 것입니다. 특정 함수는 올바르게 검사되기 위해 추론된 T 타입이 필요할 수 있기 때문에 이렇게 동작합니다. 우리의 경우 consume 함수를 분석하려면 T의 타입을 알아야 합니다.
이런 함수들을 컨텍스트에 민감한 함수 라고 부릅니다. 기본적으로 명시적 타입이 없는 매개변수를 가진 함수입니다. 결국 타입 시스템은 이 매개변수들의 타입을 알아내야 하지만, 이는 제네릭 함수에서의 추론 방식과 약간 충돌합니다. 두 메커니즘이 서로 다른 방향으로 타입을 "당기기" 때문입니다.
function callFunc<T>(callback: (x: T) => void, value: T) {
return callback(value);
}
callFunc(x => x.toFixed(), 42);
// ^
// We need to figure out the type of `x` here,
// but we also need to figure out the type of `T` to check the callback.
이 문제를 해결하기 위해 TypeScript는 타입 인수 추론 중에는 컨텍스트에 민감한 함수를 건너뛰고, 대신 다른 인수들로부터 먼저 검사와 추론을 수행합니다. 컨텍스트에 민감한 함수를 건너뛰는 것으로 충분하지 않다면, 추론은 검사되지 않은 인수 전체에 대해 인수 목록의 왼쪽에서 오른쪽 순으로 계속됩니다. 바로 위 예제에서는 TypeScript가 T를 추론할 때 콜백을 건너뛰고, 두 번째 인수 42를 보고 T가 number라고 추론합니다. 그런 다음 콜백을 검사하러 돌아왔을 때 (x: number) => void라는 컨텍스트 타입을 가지게 되므로 x도 number라고 추론할 수 있습니다.
그렇다면 앞선 예제에서는 무엇이 일어나고 있었을까요?
// Arrow syntax - no errors.
callIt({
consume: y => y.toFixed(),
produce: (x: number) => x * 2,
});
// Method syntax - errors!
callIt({
consume(y) { return y.toFixed(); },
// ~
// error: 'y' is of type 'unknown'.
produce(x: number) { return x * 2; },
});
두 예제 모두에서 produce에는 명시적으로 타입이 지정된 x 매개변수를 가진 함수가 할당됩니다. 그렇다면 동일하게 검사되어야 하지 않을까요?
문제는 미묘합니다. 대부분의 함수(메서드 문법을 사용하는 함수들처럼)는 암묵적인 this 매개변수를 가지지만, 화살표 함수는 그렇지 않습니다. this를 사용하는 모든 경우는 T의 타입을 "당겨와야" 할 수 있습니다. 예를 들어 포함하는 객체 리터럴의 타입을 알기 위해 다시 consume의 타입이 필요할 수 있고, consume은 T를 사용합니다.
하지만 우리는 this를 사용하지 않습니다! 런타임에 함수가 this 값을 가질 수는 있겠지만, 실제로 전혀 사용되지 않습니다.
TypeScript 6.0은 함수가 컨텍스트에 민감한지 여부를 결정할 때 이 점을 고려합니다. 함수 안에서 this가 실제로 사용되지 않는다면, 그 함수는 컨텍스트에 민감한 것으로 간주되지 않습니다. 즉, 타입 추론 시 이런 함수들이 더 높은 우선순위를 갖게 되며, 위의 예제들이 이제 모두 동작합니다!
이 변경은 Mateusz Burzyński의 작업 덕분에 제공되었습니다.
#/로 시작하는 하위 경로 importNode.js가 모듈 지원을 추가했을 때, "subpath imports"라는 기능도 추가했습니다. 이는 기본적으로 imports라는 필드이며, 패키지가 자신의 내부 모듈에 대한 별칭을 만들 수 있게 해줍니다.
{
"name": "my-package",
"type": "module",
"imports": {
"#root/*": "./dist/*"
}
}
이를 통해 my-package 내부의 모듈은 #root/로 시작하는 경로에서 import할 수 있습니다.
import * as utils from "#root/utils.js";
다음과 같은 상대 경로를 사용하는 대신 말입니다.
import * as utils from "../../utils.js";
이 기능의 작은 불편 중 하나는, 개발자가 하위 경로 import를 지정할 때 항상 # 뒤에 무언가 를 적어야 했다는 점입니다. 여기서는 root를 사용했지만, ./dist/ 외에 매핑하는 디렉터리가 따로 있는 것도 아니므로 약간 불필요합니다.
번들러를 사용해 본 개발자들은 긴 상대 경로를 피하기 위해 경로 매핑을 사용하는 데도 익숙합니다. 번들러에서 익숙한 관례는 간단한 @/를 접두사로 사용하는 것이었습니다. 안타깝게도 하위 경로 import는 애초에 #/로 시작할 수 없어서, 프로젝트에 이를 도입하려는 개발자들에게 많은 혼란을 주었습니다.
하지만 최근 Node.js가 #/로 시작하는 하위 경로 import를 지원하게 되었습니다. 덕분에 패키지는 추가 세그먼트를 넣지 않고도 간단한 #/ 접두사를 하위 경로 import에 사용할 수 있습니다.
{
"name": "my-package",
"type": "module",
"imports": {
"#/*": "./dist/*"
}
}
이 기능은 최신 Node.js 20 릴리스에서 지원되므로, TypeScript도 이제 --moduleResolution 설정에서 nodenext 및 bundler 옵션 아래에서 이를 지원합니다.
이 작업은 magic-akari 덕분에 이루어졌으며, 구현 pull request는 여기에서 볼 수 있습니다.
--moduleResolution bundler와 --module commonjs의 조합TypeScript의 --moduleResolution bundler 설정은 이전에는 --module esnext 또는 --module preserve와만 함께 사용할 수 있었습니다. 하지만 --moduleResolution node(즉 --moduleResolution node10)가 사용 중단되면서, 이 새로운 조합이 많은 프로젝트에 가장 적합한 업그레이드 경로가 되는 경우가 많습니다.
프로젝트는 대개 다음 중 하나로의 마이그레이션을 계획하고 싶어 합니다.
--module preserve 및 --moduleResolution bundler--module nodenext이는 프로젝트 유형(예: 번들된 웹 앱, Bun 앱, 또는 Node.js 앱)에 따라 달라집니다.
자세한 내용은 이 구현 pull request에서 확인할 수 있습니다.
--stableTypeOrdering 플래그TypeScript의 네이티브 포트에 대한 지속적인 작업의 일환으로, 6.0에서 7.0으로의 마이그레이션을 돕기 위한 --stableTypeOrdering이라는 새 플래그를 도입했습니다.
현재 TypeScript는 타입을 처음 만나는 순서대로 타입 ID(내부 추적 번호)를 할당하고, 이 ID를 사용해 유니온 타입을 일관되게 정렬합니다. 속성에 대해서도 비슷한 과정이 일어납니다. 그 결과 프로그램 안에서 선언되는 순서는 선언 출력 같은 부분에 다소 예상 밖의 영향을 줄 수 있습니다.
예를 들어 다음 파일의 declaration emit을 생각해 보겠습니다.
// Input: some-file.ts
export function foo(condition: boolean) {
return condition ? 100 : 500;
}
// Output: some-file.d.ts
export declare function foo(condition: boolean): 100 | 500;
// ^^^^^^^^^
// Note the order of this union: 100, then 500.
foo 위에 관련 없는 const를 하나 추가하면 declaration emit이 바뀝니다.
// Input: some-file.ts
const x = 500;
export function foo(condition: boolean) {
return condition ? 100 : 500;
}
// Output: some-file.d.ts
export declare function foo(condition: boolean): 500 | 100;
// ^^^^^^^^^
// Note the change in order here.
이는 const x 선언을 분석할 때 리터럴 타입 500이 먼저 처리되었기 때문에 100보다 더 낮은 타입 ID를 받기 때문입니다. 매우 드문 경우에는 이런 정렬 순서 변화가 프로그램 처리 순서에 따라 오류를 나타나게 하거나 사라지게 만들 수도 있지만, 일반적으로 이런 정렬은 출력된 declaration 파일이나 편집기에 표시되는 타입의 방식에서 주로 눈에 띕니다.
TypeScript 7의 주요 아키텍처 개선 중 하나는 병렬 타입 검사이며, 이는 전체 검사 시간을 크게 개선합니다. 하지만 병렬성은 새로운 과제를 가져옵니다. 서로 다른 타입 체커가 노드, 타입, 심볼을 서로 다른 순서로 방문하면, 이 구성 요소들에 할당되는 내부 ID가 비결정적이 됩니다. 그러면 혼란스러운 비결정적 출력이 발생하여, 같은 프로그램 안에서 내용이 동일한 두 파일이 서로 다른 declaration 파일을 생성하거나, 심지어 같은 파일을 분석하면서 서로 다른 오류를 계산할 수도 있습니다. 이를 해결하기 위해 TypeScript 7.0은 내부 객체(예: 타입과 심볼)를 객체의 내용을 기반으로 한 결정적 알고리즘에 따라 정렬합니다. 이렇게 하면 객체가 언제 어떻게 만들어졌는지와 관계없이 모든 체커가 동일한 객체 순서를 만나게 됩니다. 그 결과 위 예제에서는 TypeScript 7이 항상 100 | 500을 출력하게 되어, 정렬 불안정성이 완전히 사라집니다.
즉, TypeScript 6과 7은 때때로 서로 다른 정렬 순서를 표시할 수 있으며 실제로 그렇습니다. 이런 정렬 변경은 거의 항상 무해하지만, 실행 간 컴파일러 출력을 비교하는 경우(예를 들어 6.0과 7.0에서 출력된 declaration 파일을 비교하는 경우) 이러한 차이 때문에 노이즈가 많이 생겨 정확성을 평가하기 어려워질 수 있습니다. 드물게는 정렬 순서 변화 때문에 타입 오류가 나타나거나 사라지는 경우도 있는데, 이는 더 혼란스러울 수 있습니다.
이 상황을 돕기 위해 6.0에서는 새 --stableTypeOrdering 플래그를 지정할 수 있습니다. 이렇게 하면 6.0의 타입 정렬 동작이 7.0과 일치하게 되어, 두 코드베이스 사이의 차이가 줄어듭니다. 다만 이 플래그를 항상 사용하는 것은 꼭 권장하지는 않는데, 타입 검사 속도가 상당히 느려질 수 있기 때문입니다(코드베이스에 따라 최대 25%).
--stableTypeOrdering을 사용할 때 타입 오류가 발생한다면, 이는 보통 추론 차이 때문입니다. --stableTypeOrdering 없이 이전에 이루어졌던 추론은 프로그램 안의 현재 타입 정렬에 우연히 기대어 동작했던 것입니다. 이를 해결하려면 어딘가에 명시적 타입을 제공하는 것이 도움이 되는 경우가 많습니다. 흔히 이는 타입 인수입니다.
- someFunctionCall(/*...*/);
+ someFunctionCall<SomeExplicitType>(/*...*/);
또는 호출에 전달하려는 인수에 대한 변수 주석일 수 있습니다.
- const someVariable = { /*... some complex object ...*/ };
+ const someVariable: SomeExplicitType = { /*... some complex object ...*/ };
someFunctionCall(someVariable);
이 플래그는 6.0과 7.0의 차이를 진단하는 데만 도움을 주기 위한 것이며, 장기적으로 사용하는 기능으로 의도된 것은 아니라는 점에 유의하세요.
이 pull-request에서 더 자세히 볼 수 있습니다.
target과 lib를 위한 es2025 옵션TypeScript 6.0은 target과 lib 모두에 대해 es2025 옵션 지원을 추가합니다. ES2025에는 새로운 JavaScript 언어 기능은 없지만, 이 새 target은 내장 API에 대한 새 타입(예: RegExp.escape)을 추가하고 몇몇 선언을 esnext에서 es2025로 이동합니다(예: Promise.try, Iterator 메서드, Set 메서드). 새 target을 활성화하는 작업은 Kenta Moriuchi의 기여로 이루어졌습니다.
Temporal을 위한 새 타입오랫동안 기다려 온 Temporal 제안이 stage 4에 도달했으며, 향후 ECMAScript 표준의 일부가 될 예정입니다. TypeScript 6.0은 이제 Temporal API에 대한 내장 타입을 포함하므로, --target esnext 또는 "lib": ["esnext"](또는 더 세분화된 esnext.temporal)를 통해 오늘 바로 TypeScript 코드에서 사용할 수 있습니다.
let yesterday = Temporal.Now.instant().subtract({
hours: 24,
});
let tomorrow = Temporal.Now.instant().add({
hours: 24,
});
console.log(`Yesterday: ${yesterday}`);
console.log(`Tomorrow: ${tomorrow}`);
Temporal은 이미 여러 런타임에서 사용할 수 있으며, stage 4 상태가 되었기 때문에 이제 공식적으로 JavaScript 언어의 일부가 되었습니다. Temporal API 문서는 MDN에서 확인할 수 있습니다.
이 작업은 GitHub 사용자 Renegade334의 기여로 이루어졌습니다.
getOrInsert)Map에서 흔한 패턴 중 하나는 키가 존재하는지 확인하고, 없으면 기본값을 설정한 뒤 가져오는 것입니다.
function processOptions(compilerOptions: Map<string, unknown>) {
let strictValue: unknown;
if (compilerOptions.has("strict")) {
strictValue = compilerOptions.get("strict");
}
else {
strictValue = true;
compilerOptions.set("strict", strictValue);
}
// ...
}
이 패턴은 번거로울 수 있습니다. ECMAScript의 "upsert" 제안이 최근 stage 4에 도달했고, Map과 WeakMap에 2개의 새 메서드를 도입했습니다.
getOrInsertgetOrInsertComputed이 메서드들은 esnext lib에 추가되었으므로, TypeScript 6.0에서 즉시 사용할 수 있습니다.
getOrInsert를 사용하면 위 코드를 다음과 같이 바꿀 수 있습니다.
function processOptions(compilerOptions: Map<string, unknown>) {
let strictValue = compilerOptions.getOrInsert("strict", true);
// ...
}
getOrInsertComputed도 비슷하게 동작하지만, 기본값을 계산하는 비용이 큰 경우(예: 많은 계산, 할당이 필요하거나 오래 걸리는 동기 I/O를 수행하는 경우)를 위한 것입니다. 대신 이 메서드는 키가 아직 존재하지 않을 때만 호출되는 콜백을 받습니다.
someMap.getOrInsertComputed("someKey", () => {
return computeSomeExpensiveValue(/*...*/);
});
이 콜백은 키도 인수로 받기 때문에, 기본값이 키를 기반으로 하는 경우에 유용할 수 있습니다.
someMap.getOrInsertComputed(someKey, computeSomeExpensiveDefaultValue);
function computeSomeExpensiveValue(key: string) {
// ...
}
이 업데이트는 GitHub 사용자 Renegade334의 기여로 이루어졌습니다.
RegExp.escape정규 표현식 안에서 매칭할 리터럴 문자열을 구성할 때는 *, +, ?, (, ) 같은 특수 정규 표현식 문자를 이스케이프하는 것이 중요합니다. RegExp Escaping ECMAScript 제안이 stage 4에 도달했고, 이를 대신 처리해 주는 새 RegExp.escape 함수를 도입했습니다.
function matchWholeWord(word: string, text: string) {
const escapedWord = RegExp.escape(word);
const regex = new RegExp(`\\b${escapedWord}\\b`, "g");
return text.match(regex);
}
RegExp.escape는 es2025 lib에서 사용할 수 있으므로, TypeScript 6.0에서 오늘 바로 사용할 수 있습니다.
이 작업은 Kenta Moriuchi의 기여로 이루어졌습니다.
dom lib에 이제 dom.iterable과 dom.asynciterable이 포함됩니다TypeScript의 lib 옵션을 사용하면 대상 런타임이 어떤 전역 선언을 갖는지 지정할 수 있습니다. 그중 하나가 웹 환경(즉 DOM API를 구현하는 브라우저)을 나타내는 dom입니다. 이전에는 DOM API의 일부가 dom.iterable과 dom.asynciterable로 분리되어 있었는데, 이는 Iterable과 AsyncIterable을 지원하지 않는 환경을 위한 것이었습니다. 그 결과 NodeList나 HTMLCollection 같은 DOM 컬렉션에서 반복 메서드를 사용하려면 dom.iterable을 명시적으로 추가해야 했습니다.
TypeScript 6.0에서는 lib.dom.iterable.d.ts와 lib.dom.asynciterable.d.ts의 내용이 lib.dom.d.ts에 완전히 포함됩니다. 구성 파일의 "lib" 배열에서 여전히 dom.iterable과 dom.asynciterable을 참조할 수는 있지만, 이제 이들은 빈 파일일 뿐입니다.
// Before TypeScript 6.0, this required "lib": ["dom", "dom.iterable"]
// Now it works with just "lib": ["dom"]
for (const element of document.querySelectorAll("div")) {
console.log(element.textContent);
}
이는 주요 최신 브라우저 중 이런 기능이 없는 경우가 없기 때문에, 흔한 혼란 지점을 제거해 주는 삶의 질 개선입니다. 이미 dom과 dom.iterable을 함께 포함하고 있었다면 이제 dom만으로 단순화할 수 있습니다.
자세한 내용은 이 이슈와 그에 대한 pull request에서 확인할 수 있습니다.
TypeScript 6.0은 TypeScript 컴파일러의 향후 네이티브 포트인 TypeScript 7.0에 개발자들이 대비할 수 있도록 설계된 중요한 전환 릴리스입니다. TypeScript 6.0은 기존 TypeScript 지식과의 완전한 호환성을 유지하고 TypeScript 5.9와도 API 호환성을 계속 유지하지만, 이번 릴리스에는 변화하는 JavaScript 생태계를 반영하고 TypeScript 7.0의 토대를 마련하는 여러 호환성 깨짐 변경과 사용 중단 항목이 포함됩니다.
TypeScript 5.0 이후 지난 2년 동안, 개발자들이 JavaScript를 작성하고 배포하는 방식에는 지속적인 변화가 있었습니다.
tsconfig.json은 거의 보편적인 구성 메커니즘이 되었습니다.그래서 TypeScript 6.0과 7.0은 이런 현실을 염두에 두고 설계되었습니다. TypeScript 6.0에서는 tsconfig에 "ignoreDeprecations": "6.0"을 설정하면 이러한 사용 중단을 무시할 수 있습니다. 하지만 TypeScript 7.0은 이 사용 중단된 옵션들을 전혀 지원하지 않을 것이라는 점에 유의하세요.
필요한 몇몇 조정은 codemod나 도구로 자동 수행할 수 있습니다. 예를 들어 실험적인 ts5to6 도구는 코드베이스 전반에 걸쳐 baseUrl과 rootDir를 자동으로 조정할 수 있습니다.
아래에서 구체적인 조정을 다루겠지만, 일부 사용 중단 및 동작 변경은 근본 원인을 직접 가리키는 오류 메시지를 반드시 제공하지는 않는다는 점을 먼저 짚고 넘어가야 합니다. 따라서 많은 프로젝트가 최소한 다음 중 하나를 수행해야 할 것이라는 점을 먼저 말씀드립니다.
"types" 배열을 설정합니다. 일반적으로는 "types": ["node"]입니다."types": ["*"]를 사용하면 5.9의 동작으로 되돌릴 수 있지만, 빌드 성능과 예측 가능성을 개선하기 위해 명시적 배열을 사용하는 것을 권장합니다.
보통 누락된 식별자나 해석되지 않은 내장 모듈과 관련된 타입 오류가 매우 많이 보인다면 이것이 원인일 가능성이 큽니다.
rootDir 추론에 의존하고 있었다면 "rootDir": "./src"를 설정합니다.보통 ./dist/index.js 대신 ./dist/src/index.js에 파일이 기록되는 것을 보면 이 문제가 원인임을 알 수 있습니다.
여러 컴파일러 옵션이 이제 현대적인 개발 관행을 더 잘 반영하도록 기본값이 업데이트되었습니다.
strict의 기본값이 이제 true입니다: 더 엄격한 타입 지정에 대한 수요는 계속 증가하고 있으며, 대부분의 새 프로젝트는 strict 모드를 켜길 원한다는 점을 확인했습니다. 이미 "strict": true를 사용하고 있었다면 아무것도 바뀌지 않습니다. 이전 기본값인 false에 의존하고 있었다면 tsconfig.json에 "strict": false를 명시적으로 설정해야 합니다.
module의 기본값이 esnext입니다: 마찬가지로 새 기본 module은 esnext이며, 이는 이제 ESM이 지배적인 모듈 형식이라는 점을 반영합니다.
target의 기본값은 현재 연도의 ES 버전입니다: 새 기본 target은 지원되는 가장 최신 ECMAScript 명세 버전입니다(사실상 부동 target). 현재는 es2025입니다. 이는 대부분의 개발자가 evergreen 런타임으로 배포하며 이전 ECMAScript 버전으로 트랜스파일할 필요가 없다는 현실을 반영합니다.
noUncheckedSideEffectImports의 기본값이 이제 true입니다: 이는 부작용만 있는 import에서 오타로 인한 문제를 잡는 데 도움이 됩니다.
libReplacement의 기본값이 이제 false입니다: 이 플래그는 이전에 매 실행마다 많은 모듈 해석 실패를 유발했고, 그 결과 --watch 및 편집기 시나리오에서 감시해야 하는 위치 수도 늘어났습니다. 새 프로젝트에서 libReplacement는 다른 명시적 구성이 있을 때까지는 아무 일도 하지 않으므로, 기본 성능 향상을 위해 기본적으로 꺼 두는 것이 합리적입니다.
이 새로운 기본값들 때문에 프로젝트가 깨진다면, tsconfig.json에서 이전 값을 명시적으로 지정할 수 있습니다.
rootDir의 기본값이 이제 .입니다rootDir는 출력 디렉터리를 기준으로 출력 파일의 디렉터리 구조를 제어합니다. 이전에는 rootDir를 지정하지 않으면 모든 non-declaration 입력 파일의 공통 디렉터리를 기준으로 추론되었습니다. 하지만 이는 프로젝트를 실제로 로드하고 파싱해 보기 전에는 어떤 파일이 프로젝트에 속하는지 알 수 없게 만드는 경우가 많았습니다. 또한 프로그램의 모든 파일 경로를 분석해 공통 소스 디렉터리를 추론하느라 TypeScript가 더 많은 시간을 쓰게 만들었습니다.
TypeScript 6.0에서는 기본 rootDir가 항상 tsconfig.json 파일이 들어 있는 디렉터리가 됩니다. rootDir는 tsconfig.json 없이 명령줄에서 tsc를 사용할 때만 추론됩니다.
tsconfig.json 디렉터리보다 더 깊은 수준에 소스 파일이 있고, TypeScript가 소스 파일의 공통 루트 디렉터리를 추론하길 기대하고 있었다면 rootDir를 명시적으로 설정해야 합니다.
{
"compilerOptions": {
// ...
+ "rootDir": "./src"
},
"include": ["./src"]
}
마찬가지로 tsconfig.json이 자신이 포함된 디렉터리 밖의 파일을 참조하고 있었다면, 그 파일들을 포함하도록 rootDir를 조정해야 합니다.
{
"compilerOptions": {
// ...
+ "rootDir": "../src"
},
"include": ["../src/**/*.tests.ts"]
}
types의 기본값이 이제 []입니다tsconfig.json에서 compilerOptions의 types 필드는 컴파일 중 전역 스코프에 포함할 패키지 이름 목록을 지정합니다. 일반적으로 node_modules의 패키지는 소스 코드의 import를 통해 자동으로 포함됩니다. 하지만 편의를 위해 TypeScript는 기본적으로 node_modules/@types의 모든 패키지도 포함했기 때문에, @types/node의 process나 "fs" 모듈, 혹은 @types/jest의 describe와 it 같은 전역 선언을 직접 import하지 않고도 사용할 수 있었습니다.
어떤 의미에서는, 이전의 types 기본값은 "node_modules/@types 안의 모든 것을 열거한다"에 가까웠습니다. 이는 매우 비용이 많이 들 수 있습니다. 요즘 일반적인 저장소 구성에서는 전이적으로 수백 개의 @types 패키지를 끌어오는 경우가 많으며, 특히 node_modules가 평탄화된 다중 프로젝트 워크스페이스에서는 더 그렇습니다. 현대적인 프로젝트는 거의 항상 @types/node, @types/jest, 또는 전역에 영향을 주는 소수의 일반 패키지만 필요로 합니다.
TypeScript 6.0에서 기본 types 값은 [](빈 배열)가 됩니다. 이 변경은 프로젝트가 빌드 시 의도치 않게 수백 또는 수천 개의 불필요한 declaration 파일을 끌어오는 일을 막아 줍니다. 저희가 살펴본 많은 프로젝트는 types를 적절히 설정하는 것만으로도 빌드 시간이 20-50% 개선되었습니다.
이 변경은 많은 프로젝트에 영향을 줄 것입니다. 아마 "types": ["node"] 또는 몇 가지를 추가해야 할 가능성이 높습니다.
{
"compilerOptions": {
// Explicitly list the @types packages you need
+ "types": ["node", "jest"]
}
}
기존의 열거 동작을 다시 켜기 위해 * 항목을 지정할 수도 있습니다.
{
"compilerOptions": {
// Load ALL the types - the default from TypeScript 5.9 and before.
+ "types": ["*"]
}
}
다음과 같은 새 오류 메시지가 나타난다면,
Cannot find module '...' or its corresponding type declarations.
Cannot find name 'fs'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
Cannot find name 'path'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
Cannot find name 'Bun'. Do you need to install type definitions for Bun? Try `npm i --save-dev @types/bun` and then add 'bun' to the types field in your tsconfig.
Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.
아마도 types 필드에 몇 가지 항목을 추가해야 한다는 뜻일 가능성이 큽니다.
자세한 내용은 여기의 제안과 구현 pull request를 참조하세요.
target: es5ECMAScript 5 target은 오랫동안 레거시 브라우저 지원을 위해 중요했지만, 그 후속인 ECMAScript 2015(ES6)는 이미 10년 넘게 전에 출시되었고, 모든 최신 브라우저는 수년 동안 이를 지원해 왔습니다. Internet Explorer의 퇴장과 evergreen 브라우저의 보편화로 인해, 오늘날 ES5 출력을 필요로 하는 경우는 매우 드뭅니다.
이제 TypeScript의 최저 target은 ES2015가 되며, target: es5 옵션은 사용 중단됩니다. target: es5를 사용하고 있었다면 더 새로운 target으로 마이그레이션하거나 외부 컴파일러를 사용해야 합니다. 여전히 ES5 출력이 필요하다면, TypeScript 소스를 직접 컴파일하거나 TypeScript 출력 결과를 후처리할 외부 컴파일러를 사용하는 것을 권장합니다.
이 사용 중단에 대한 자세한 내용은 여기와 구현 pull request에서 확인할 수 있습니다.
--downlevelIteration--downlevelIteration은 ES5 출력에만 영향을 주며, --target es5가 사용 중단되었으므로 더 이상 목적이 없습니다.
미묘한 점으로, --target es2015와 함께 --downlevelIteration false를 사용하는 것은 아무 효과가 없었음에도 TypeScript 5.9 이전에서는 오류가 나지 않았습니다. TypeScript 6.0에서는 --downlevelIteration을 설정하는 것 자체가 사용 중단 오류를 일으킵니다.
구현은 여기에서 확인할 수 있습니다.
--moduleResolution node (즉 --moduleResolution node10)--moduleResolution node는 Node.js의 모듈 해석 알고리즘 중 Node.js 10의 동작을 가장 정확히 반영하는 특정 버전을 인코딩하고 있었습니다. 불행히도 이 대상(그리고 이름)은 그 이후 Node.js 해석 알고리즘에 이루어진 많은 업데이트를 반영하지 못하며, 더 이상 현대적인 Node.js 버전의 동작을 잘 표현하지 못합니다.
TypeScript 6.0에서 --moduleResolution node(구체적으로는 --moduleResolution node10)는 사용 중단됩니다. --moduleResolution node를 사용하던 사용자는, Node.js를 직접 대상으로 한다면 보통 --moduleResolution nodenext로, 번들러나 Bun을 사용할 계획이라면 --moduleResolution bundler로 옮겨야 합니다.
자세한 내용은 이 이슈와 해당 pull request에서 확인할 수 있습니다.
module의 amd, umd, systemjs 값다음 플래그 값은 더 이상 지원되지 않습니다.
--module amd--module umd--module systemjs--module noneAMD, UMD, SystemJS는 브라우저에 네이티브 모듈 지원이 없던 초기 JavaScript 모듈 시대에 중요했습니다. none의 의미는 애초에 명확하게 정의된 적이 없고 종종 혼란을 낳았습니다. 오늘날 브라우저와 Node.js는 모두 ESM을 보편적으로 지원하며, import map과 번들러 모두 빈틈을 메우는 선호되는 방식이 되었습니다. 여전히 이런 모듈 시스템을 대상으로 한다면, 적절한 ECMAScript 모듈 출력 target으로 마이그레이션하거나, 번들러 또는 다른 컴파일러를 채택하거나, 마이그레이션할 수 있을 때까지 TypeScript 5.x에 머무르는 것을 고려하세요.
이는 또한 amd-module 지시문 지원도 제거됨을 의미하며, 이제 더 이상 아무 효과도 없습니다.
자세한 내용은 제안 이슈와 구현 pull request를 참조하세요.
--baseUrlbaseUrl 옵션은 가장 흔하게 paths와 함께 사용되며, 보통 paths의 모든 값에 대한 접두사로 사용됩니다. 하지만 불행히도 baseUrl은 모듈 해석에서 탐색 루트로도 간주됩니다.
예를 들어 다음 tsconfig.json이 있다고 해보겠습니다.
{
"compilerOptions": {
// ...
"baseUrl": "./src",
"paths": {
"@app/*": ["app/*"],
"@lib/*": ["lib/*"]
}
}
}
그리고 다음과 같은 import가 있다면,
import * as someModule from "someModule.js";
개발자가 단지 @app/과 @lib/로 시작하는 모듈에 대한 매핑만 추가하려 했더라도, TypeScript는 이를 아마 src/someModule.js로 해석할 것입니다.
최선의 경우에도 이것은 번들러가 무시할 "보기 좋지 않은" 경로를 자주 만들어 냅니다. 하지만 더 흔하게는, 런타임에서 절대 동작하지 않았을 많은 import 경로가 TypeScript에서는 "괜찮은 것"으로 간주되는 결과를 낳았습니다.
path 매핑은 오래전부터 baseUrl 지정이 필요하지 않았고, 실제로 baseUrl을 사용하는 대부분의 프로젝트는 이를 단지 paths 항목의 접두사로만 사용합니다. TypeScript 6.0에서 baseUrl은 사용 중단되며, 더 이상 모듈 해석의 탐색 루트로 간주되지 않습니다.
경로 매핑 항목의 접두사로 baseUrl을 사용하던 개발자는 baseUrl을 제거하고 그 접두사를 paths 항목에 추가하면 됩니다.
{
"compilerOptions": {
// ...
- "baseUrl": "./src",
"paths": {
- "@app/*": ["app/*"],
- "@lib/*": ["lib/*"]
+ "@app/*": ["./src/app/*"],
+ "@lib/*": ["./src/lib/*"]
}
}
}
실제로 baseUrl을 탐색 루트로 사용하고 있었던 개발자도 이전 동작을 유지하기 위해 명시적 경로 매핑을 추가할 수 있습니다.
{
"compilerOptions": {
// ...
"paths": {
// A new catch-all that replaces the baseUrl:
"*": ["./src/*"],
// Every other path now has an explicit common prefix:
"@app/*": ["./src/app/*"],
"@lib/*": ["./src/lib/*"],
}
}
}
하지만 이런 경우는 매우 드뭅니다. 대부분의 개발자에게는 baseUrl을 제거하고 paths 항목에 적절한 접두사를 추가하는 것을 권장합니다.
자세한 내용은 이 이슈와 해당 pull request를 참조하세요.
--moduleResolution classicmoduleResolution: classic 설정은 제거되었습니다. classic 해석 전략은 TypeScript의 원래 모듈 해석 알고리즘으로, Node.js의 해석 알고리즘이 사실상의 표준이 되기 이전의 것입니다. 오늘날 실질적인 모든 사용 사례는 nodenext 또는 bundler로 해결됩니다. classic을 사용하고 있었다면 이 현대적인 해석 전략 중 하나로 마이그레이션하세요.
자세한 내용은 이 이슈와 구현 pull request를 참조하세요.
--esModuleInterop false와 --allowSyntheticDefaultImports false다음 설정은 더 이상 false로 설정할 수 없습니다.
esModuleInteropallowSyntheticDefaultImportsesModuleInterop과 allowSyntheticDefaultImports는 기존 프로젝트를 깨뜨리지 않기 위해 원래는 선택적으로 켜는 기능이었습니다. 그러나 이들이 활성화하는 동작은 수년 동안 권장 기본값이었습니다. 이를 false로 설정하면 ESM에서 CommonJS 모듈을 소비할 때 미묘한 런타임 문제가 자주 발생했습니다. TypeScript 6.0에서는 더 안전한 상호 운용 동작이 항상 활성화됩니다.
이전 동작에 의존하는 import가 있다면 다음과 같이 조정해야 할 수 있습니다.
// Before (with esModuleInterop: false)
import * as express from "express";
// After (with esModuleInterop always enabled)
import express from "express";
자세한 내용은 이 이슈와 구현 pull request를 참조하세요.
--alwaysStrict falsealwaysStrict 플래그는 "use strict"; 지시문의 추론과 출력과 관련이 있습니다. TypeScript 6.0에서는 모든 코드가 JavaScript strict mode에 있다고 가정됩니다. 이는 특히 예약어 주변의 문법적 모서리 사례에 가장 눈에 띄게 영향을 주는 JS 의미 체계 집합입니다. await, static, private, public 같은 예약어를 일반 식별자로 사용하는 "sloppy mode" 코드가 있다면 이름을 바꿔야 합니다. 비엄격 코드에서 this의 의미에 관한 미묘한 동작에 의존하고 있었다면 코드 조정이 필요할 수도 있습니다.
자세한 내용은 이 이슈와 해당 pull request를 참조하세요.
outFile--outFile 옵션은 TypeScript 6.0에서 제거되었습니다. 이 옵션은 원래 여러 입력 파일을 하나의 출력 파일로 연결하기 위해 설계되었습니다. 그러나 이제 Webpack, Rollup, esbuild, Vite, Parcel 등과 같은 외부 번들러가 이 작업을 더 빠르고 더 잘, 훨씬 더 많은 설정 가능성과 함께 수행합니다. 이 옵션을 제거하면 구현이 단순해지고 TypeScript가 가장 잘하는 일, 즉 타입 검사와 declaration emit에 집중할 수 있게 됩니다. 현재 --outFile을 사용 중이라면 외부 번들러로 마이그레이션해야 합니다. 대부분의 최신 번들러는 기본적으로 훌륭한 TypeScript 지원을 제공합니다.
module 문법초기 TypeScript 버전에서는 module 키워드를 사용해 네임스페이스를 선언했습니다.
// ❌ Deprecated syntax - now an error
module Foo {
export const bar = 10;
}
이 문법은 이후 namespace 키워드를 사용하는 현대적인 선호 형식의 별칭이 되었습니다.
// ✅ The correct syntax
namespace Foo {
export const bar = 10;
}
namespace가 도입되었을 때 module 문법은 단지 권장되지 않는 것으로만 취급되었습니다. 몇 년 전부터는 TypeScript 언어 서비스가 이 키워드를 사용 중단으로 표시하고 대신 namespace를 제안하기 시작했습니다.
TypeScript 6.0에서는 namespace가 기대되는 위치에서 module을 사용하는 것이 이제 강한 사용 중단이 됩니다. 이 변경은 module 블록이 레거시 TypeScript 문법과 충돌할 수 있는 잠재적인 ECMAScript 제안이기 때문에 필요합니다.
ambient module declaration 형식은 계속 완전히 지원됩니다.
// ✅ Still works perfectly
declare module "some-module" {
export function doSomething(): void;
}
자세한 내용은 이 이슈와 해당 pull request에서 확인할 수 있습니다.
asserts 키워드asserts 키워드는 import assertions 제안을 통해 JavaScript 언어에 제안되었지만, 이 제안은 결국 asserts 대신 with 키워드를 사용하는 import attributes 제안으로 바뀌었습니다.
따라서 asserts 문법은 이제 TypeScript 6.0에서 사용 중단되며, 사용하면 오류가 발생합니다.
// ❌ Deprecated syntax - now an error.
import blob from "./blahb.json" asserts { type: "json" }
// ~~~~~~~
// error: Import assertions have been replaced by import attributes. Use 'with' instead of 'asserts'.
대신 import attributes를 위한 with 문법을 사용하세요.
// ✅ Works with the new import attributes syntax.
import blob from "./blahb.json" with { type: "json" }
자세한 내용은 이 이슈와 해당 pull request를 참조하세요.
no-default-lib 지시문/// <reference no-default-lib="true"/> 지시문은 대체로 오해되고 오용되어 왔습니다. TypeScript 6.0에서는 이 지시문이 더 이상 지원되지 않습니다. 사용하고 있었다면 대신 --noLib 또는 --libReplacement를 고려하세요.
자세한 내용은 여기와 해당 pull request에서 확인할 수 있습니다.
tsconfig.json이 있을 때 명령줄 파일 지정이 이제 오류가 됩니다현재는 tsconfig.json이 있는 폴더에서 tsc foo.ts를 실행하면, 구성 파일이 완전히 무시됩니다. 입력 파일에 검사 및 출력 옵션이 적용되길 기대했다면 이는 종종 매우 혼란스러웠습니다.
TypeScript 6.0에서는 tsconfig.json이 있는 디렉터리에서 파일 인수와 함께 tsc를 실행하면, 이런 동작을 명시적으로 드러내기 위해 오류가 발생합니다.
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.
정말로 tsconfig.json을 무시하고 TypeScript 기본값으로 foo.ts만 컴파일하려는 의도였다면, 새 --ignoreConfig 플래그를 사용할 수 있습니다.
tsc --ignoreConfig foo.ts
자세한 내용은 이 이슈와 해당 pull request를 참조하세요.
TypeScript 6.0은 전환 릴리스로 설계되었습니다. TypeScript 6.0에서 사용 중단된 옵션들은 "ignoreDeprecations": "6.0"이 설정되어 있으면 오류 없이 계속 동작하지만, 해당 옵션들은 TypeScript 7.0(네이티브 TypeScript 포트)에서 완전히 제거될 것입니다. TypeScript 6.0으로 업그레이드한 뒤 사용 중단 경고가 보인다면, 프로젝트에서 TypeScript 7.0(또는 네이티브 프리뷰)를 도입하기 전에 이를 해결할 것을 강력히 권장합니다.
이제 TypeScript 6.0이 npm에서 제공되므로, 팀은 TypeScript 7.0을 안정화하는 데 집중할 예정입니다. 이는 생각보다 훨씬 가까이 와 있습니다. 몇 달 안에 릴리스를 기대하고 있으며, Microsoft 안팎의 매우 큰 코드베이스에서 이미 폭넓은 도입이 이루어지고 있습니다. 따라서 팀들이 npm의 TypeScript 7.0 네이티브 프리뷰 야간 빌드와 VS Code 확장도 함께 사용해 보시길 권장합니다. TypeScript 7.0에 대한 피드백은 큰 도움이 될 것이며, 이슈는 이슈 추적기에 제출할 수 있습니다.
그럼에도 TypeScript 6.0은 오늘 바로 도입할 수 있는 안정 릴리스이며, 즉시 사용할 수 있는 여러 개선 사항과 새 기능도 포함하고 있습니다. 이 릴리스가 모두에게 매끄러운 전환이 되기를 바라며, 여러분의 사용 경험을 듣게 되길 기대합니다.
즐거운 해킹 되세요!
– Daniel Rosenwasser 및 TypeScript 팀
Category

수석 제품 관리자
Daniel Rosenwasser는 TypeScript 팀의 제품 관리자입니다. 그는 프로그래밍 언어, 컴파일러, 그리고 뛰어난 개발자 도구에 대한 열정을 가지고 있습니다.