DefinitelyTyped와 @types
공동체가 관리하는 DefinitelyTyped와 @types 패키지가 무엇인지, 라이브러리의 타입을 자체 선언과 @types, 직접 선언 가운데 어디서 구할지 고르는 순서, 명시적 import와 전역 포함이라는 두 경로를 정하는 types와 typeRoots 옵션, TypeScript 6.0에서 바뀐 기본값을 다룹니다.
자바스크립트로 배포된 라이브러리는 수없이 많고, 그 모두에 선언 파일을 직접 써 줄 수는 없습니다. 그래서 타입스크립트 사용자들은 널리 쓰이는 라이브러리의 선언 파일을 한곳에 모아 함께 관리하는데, 그 저장소가 DefinitelyTyped입니다. 여기서 관리하는 선언은 @types라는 이름 공간의 npm 패키지로 배포됩니다.
이 글은 @types 패키지를 설치하고 쓰는 방법을 보고, 라이브러리의 타입을 어디서 구할지 고르는 순서를 정리합니다. 마지막으로 types와 typeRoots 옵션이 무엇을 정하고 무엇은 정하지 않는지, TypeScript 6.0에서 바뀐 기본값과 함께 확인합니다.
DefinitelyTyped와 @types 패키지
DefinitelyTyped는 자바스크립트 라이브러리의 선언 파일을 모아 둔 공개 저장소이며, 라이브러리마다 types/lodash처럼 폴더가 하나씩 있습니다. 여러 사람이 선언을 고치고 검토하면, 그 결과가 자동으로 @types/lodash 같은 npm 패키지로 배포됩니다.
@types 패키지에는 실행할 코드가 없고 선언 파일만 들어 있습니다. 그래서 라이브러리를 쓰려면 실제 구현인 라이브러리와 그 선언인 @types 패키지를 각각 설치합니다.
npm install lodash
npm install --save-dev @types/lodash--save-dev는 선언을 개발 의존성으로 기록하는 옵션입니다. 애플리케이션은 빌드할 때만 선언이 필요하므로 이렇게 두지만, 라이브러리를 만들어 배포하면서 공개한 선언이 다른 @types 패키지를 참조한다면, 사용하는 쪽에서도 그 선언을 받을 수 있도록 일반 의존성에 두어야 합니다. @types 패키지의 버전은 보통 라이브러리 버전의 앞 두 자리를 따르므로, 설치한 라이브러리와 버전이 맞는 선언을 고릅니다.
import _ from "lodash";
const total = _.sum([1, 2, 3, 4, 5]);
console.log(total);15컴파일러는 "lodash"를 해석하다가 node_modules/@types/lodash의 선언을 찾아, _.sum이 숫자 배열을 받아 number를 돌려준다는 것을 압니다. lodash는 module.exports에 함수 객체를 담는 CommonJS 패키지이므로, 7장에서 본 상호 운용 동작 덕분에 기본 가져오기로 받을 수 있습니다.
더 깊이: @types 패키지의 버전과 지원 범위
DefinitelyTyped의 안내에 따르면, @types 패키지와 라이브러리가 맞추는 것은 버전의 앞 두 자리인 주 버전과 부 버전뿐입니다. 마지막 자리는 DefinitelyTyped가 선언을 고칠 때마다 따로 올리는 번호이므로, @types/lodash의 마지막 자리가 lodash와 다르다고 해서 맞지 않는 선언은 아닙니다. 반대로 앞 두 자리가 다르다면 라이브러리의 다른 버전을 설명하는 선언일 수 있습니다.
DefinitelyTyped는 나온 지 2년이 안 된 타입스크립트 버전에서만 패키지를 검사합니다. 그 대신 @types 패키지에는 지원하는 타입스크립트 버전별 태그가 붙어 있어서, npm dist-tags @types/react처럼 태그를 확인하면 오래된 컴파일러에 맞는 선언 버전을 찾을 수 있습니다. 또 라이브러리가 자체 선언을 배포하기 시작하면 그 선언은 혼란을 막으려고 DefinitelyTyped에서 지워지고, @types에는 선언 대신 그 사실을 알리는 스텁(stub) 패키지가 올라갑니다.
타입을 어디서 구할까
새 라이브러리를 들일 때는 타입을 구하는 곳을 차례로 확인합니다.
TYPE / RUNTIME / CONTRACT
자체 선언부터 확인하고 없는 타입만 보완한다
새 라이브러리는 현재 패키지의 공개 경로와 타입 내장을 확인합니다. 없을 때 호환되는 @types를 찾고, 필요한 API를 직접 선언하거나 타입 검사를 포기하는 선택의 차이를 읽습니다.
예는 오른쪽의 선언을 사용하고, 아니요는 다음 선택으로 진행합니다. 호환 타입을 직접 기술하는 선택과 any로 검사를 포기하는 선택은 같은 안전성 수준이 아닙니다. 어느 선언도 구현을 설치하지 않습니다.
1 · BUNDLED
자체 선언이 있으면 그것을 사용한다
my-typescript-lib는 main:dist/index.js와 types:dist/index.d.ts를 제공합니다. exports의 타입 조건도 확인하며 중복되거나 불필요해진 @types 설치를 피합니다.
2 · @TYPES
구현 버전과 호환되는 선언을 찾는다
본문은 lodash 구현과 @types/lodash를 설치합니다. _.sum([1,2,3,4,5])은15이며 shuffle 결과의 타입은 number[]입니다. 선언은 실제 lodash 구현을 설치하지 않습니다.
3 · LOCAL
사용할 API를 좁게 직접 설명한다
my-custom-js-lib의 doSomething(param:string):number를 선언합니다. 이 선언으로 문자열 호출을 검사하지만 실제 함수 구현과 반환값은 별도로 확인해야 합니다.
LAST OPTION
선언 누락과 any를 구분한다
some-untyped-lib의 선언이 없으면 strict 설정에서 오류가 날 수 있습니다. 누락을 허용해 any로 사용하면 new와 메서드 호출의 타입 검사를 포기합니다.
CONSUMER
타입이 허용하는 범위를 읽는다
타입 선언이 허용하는 string shorthand까지 실제 속성 존재를 증명하지는 않습니다. default/named export와 선택한 모듈 실행 방식도 맞춰서 사용합니다.
DEPENDENCY
배포되는 선언의 의존성도 전달한다
앱 빌드용 @types는 devDependencies로 둘 수 있습니다. 배포할 .d.ts가 다른 타입 패키지를 참조하면 소비자가 받도록 dependencies 등 의존성 배치를 검토합니다.
@types는 DefinitelyTyped에서 배포하는 선언 패키지입니다. 타입 패키지 선택과 실제 JavaScript의 설치·실행은 각각 확인합니다.
먼저 라이브러리가 자체 선언을 함께 배포하는지 봅니다. 타입스크립트로 만들었거나 선언을 직접 관리하는 패키지는 package.json의 types 필드나 exports의 types 조건으로 선언 파일을 알려 주므로, @types를 따로 설치할 필요가 없습니다. 오히려 자체 선언이 생긴 뒤에도 예전의 @types가 남아 있으면 오래된 선언이 섞일 수 있으니 지웁니다.
자체 선언이 없으면 버전이 맞는 @types/라이브러리 이름이 있는지 찾습니다. 그것도 없다면 8장 3편처럼 실제로 쓰는 부분만 declare module로 직접 선언하는데, 작은 라이브러리나 쓰는 함수가 몇 개뿐일 때는 이쪽이 오히려 간단합니다.
선언을 전혀 두지 않으면, strict 설정에서는 그 모듈의 가져오기에 Could not find a declaration file for module로 시작하는 TS7016이 나옵니다. 이 진단을 끄고 모듈 전체를 any로 쓰는 방법도 있지만, 그 라이브러리의 모든 사용에서 검사를 포기하는 것이어서 마지막 선택으로 남겨 둡니다. 위 그림처럼 맞는 타입을 직접 적는 것과 any로 검사를 포기하는 것은 같은 수준의 안전이 아니며, 어느 쪽도 구현을 설치해 주지는 않습니다.
명시적 import와 전역 포함
선언이 프로그램에 들어오는 길은 두 가지입니다. 하나는 코드에서 import한 모듈의 이름을 해석하다가 선언을 찾는 길이고, 다른 하나는 import하지 않아도 전역에 이름을 더하는 선언을 처음부터 포함하는 길입니다. lodash는 앞의 길로, Node.js의 전역인 process나 테스트 도구의 전역인 expect는 뒤의 길로 들어옵니다.
types 옵션은 뒤의 길, 곧 전역에 자동으로 포함할 @types 패키지의 목록을 정합니다. TypeScript 6.0에서 이 옵션의 기본값은 빈 배열이어서, 설치한 @types 패키지라도 전역 선언은 자동으로 들어오지 않습니다.
{
"compilerOptions": {
"types": ["node", "jest"]
}
}이렇게 적으면 @types/node와 @types/jest의 전역 선언만 포함되어 process와 expect를 쓸 수 있고, 이렇게 목록을 좁혀 두면 여러 패키지의 전역 선언이 서로 부딪히는 일도 줄일 수 있습니다. 예전처럼 설치한 @types 패키지를 모두 포함하려면 "types": ["*"]로 적습니다.
typeRoots는 그 @types 패키지를 찾을 폴더의 목록입니다. 적지 않으면 node_modules/@types를 찾고, 적으면 그 목록으로 바뀌므로, 직접 만든 선언 폴더를 더하려면 ["./node_modules/@types", "./custom-types"]처럼 원래 폴더도 함께 적습니다. 두 옵션의 자세한 규칙은 공식 types 설명에 있습니다.
TYPE / RUNTIME / CONTRACT
명시적 import와 자동 전역 포함은 별도로 설정한다
types 목록은 자동으로 전역에 포함할 타입 패키지를 선택합니다. 목록에 없는 패키지를 명시적으로 import했을 때의 모듈 타입 해석까지 차단하지는 않습니다.
명시적 import와 자동 전역 포함은 독립적인 두 입력 경로이고 마지막 타입 환경에 합쳐집니다. 세로로 재배치해도 첫 경로의 출력이 두 번째 경로의 입력이 되는 간선은 없습니다. 직접 files/include로 포함한 선언은 별도로 유지합니다.
IMPORT
목록 밖의 모듈도 타입을 해석한다
types:[]이어도 import한 lodash 등의 선언은 설정에 따라 해석합니다. 전역 목록에서 빠졌다는 이유만으로 모듈 API가 any가 되지는 않습니다.
TYPES
자동 전역 패키지를 고른다
본문 types:["node","jest"]는 해당 전역 타입을 포함합니다. process·expect 같은 전역 이름과 모듈 import를 구분합니다. types:[]는 자동 전역 패키지 포함을 비웁니다.
TYPE ROOTS
루트를 추가가 아닌 목록으로 지정한다
본문은 ./node_modules/@types와 ./custom-types를 함께 지정합니다. 기본 루트에 자동으로 더하는 옵션이 아니며 커스텀 루트는 패키지별 선언 디렉터리로 구성합니다.
VERSION / LOCAL
버전 기본값과 직접 포함을 구분한다
TypeScript6.0 이상은 types의 기본값이[]입니다. 이전 기본값과 구분해 필요한 전역 패키지를 명시합니다. files/include로 직접 포함한 global.d.ts를 types:[]가 제거하는 것은 아닙니다.
types와 typeRoots는 JavaScript 번들 크기를 직접 줄이는 설정이 아닙니다. 모듈 해석 실패, 전역 이름 누락, 실제 실행 경로 오류를 각각 확인합니다.
위 그림처럼 두 길은 서로 따로 움직입니다. types 목록은 전역에 포함할 패키지를 고를 뿐이어서, 목록에 없는 패키지라도 코드에서 명시적으로 import하면 그 모듈의 선언은 그대로 찾습니다. types: []가 모든 선언을 막는 스위치는 아니며, files나 include로 직접 넣은 선언 파일도 그대로 포함됩니다.
정리
- DefinitelyTyped는 자바스크립트 라이브러리의 선언을 모아 관리하는 저장소이고, 그 선언은
@types/이름패키지로 배포되며 실행 코드는 들어 있지 않습니다. @types패키지는 라이브러리와 버전을 맞추어 개발 의존성으로 설치하되, 배포하는 선언이 참조한다면 일반 의존성에 둡니다.- 타입은 자체 선언, 버전이 맞는
@types, 쓰는 부분만의 직접 선언 순서로 구하며,any로 검사를 포기하는 것은 마지막 선택입니다. types는 전역에 자동으로 포함할@types패키지를 정하며, TypeScript 6.0에서 기본값은 빈 배열이고["*"]는 예전처럼 모두 포함합니다.- 명시적으로
import한 패키지의 선언은types목록과 관계없이 해석됩니다.
연습 문제
1. 두 경로 구분하기
lodash와 @types/lodash, @types/node가 설치되어 있고 tsconfig.json에는 types를 적지 않았습니다. TypeScript 6.0에서 import _ from "lodash"; console.log(_.sum([2, 4, 6]));와 console.log(process.version);은 각각 어떻게 되나요?
해설 보기
lodash는 명시적으로 가져왔으므로 선언을 찾아 검사를 통과하고, 실행하면 12를 출력합니다. process는 import하지 않은 전역 이름인데, types의 기본값이 빈 배열이어서 @types/node의 전역 선언이 들어오지 않으므로 Cannot find name 'process'.로 시작하는 진단이 나옵니다. "types": ["node"]를 적으면 해결되지만, import process from "node:process";처럼 가져오는 것으로는 해결되지 않습니다. "node:process" 같은 Node.js 내장 모듈은 lodash처럼 자기 이름의 @types 패키지로 해석되는 것이 아니라, @types/node가 전역에 선언해 두는 모듈이기 때문입니다.
2. 선언만 있는 실행
새로 받은 프로젝트에 @types/lodash만 설치되어 있고 lodash는 설치되지 않았습니다. 첫 문제의 lodash 코드는 검사와 실행에서 각각 어떻게 되나요?
해설 보기
@types/lodash의 선언이 있으므로 검사는 통과합니다. 하지만 @types 패키지에는 실행 코드가 없으므로, 실행하면 lodash 모듈을 찾지 못해 멈춥니다. 선언과 구현은 서로 다른 패키지이므로, package.json에 둘 다 기록되어 있는지 확인합니다.
3. 자체 선언과 남은 @types
어떤 라이브러리가 새 버전부터 자체 선언을 함께 배포하기 시작했는데, 프로젝트에는 예전에 설치한 그 라이브러리의 @types 패키지가 남아 있습니다. 무엇을 해야 하나요?
해설 보기
라이브러리가 자체 선언을 배포하면 컴파일러는 그 선언을 쓰므로, 남아 있는 @types 패키지는 필요 없고 오래된 선언이 섞이는 원인이 될 수 있습니다. 라이브러리의 package.json에서 types나 exports의 types 조건이 정말 있는지 확인한 뒤, npm uninstall @types/라이브러리 이름으로 지웁니다.
이것으로 8장의 타입 선언을 마칩니다. 다음 장의 tsconfig.json 설정에서는 지금까지 조금씩 써 온 tsconfig.json의 구조와 설정을 나누어 관리하는 방법을 정리합니다.