이전 글에서 이어집니다. 지난 시간에 타입스크립트 환경 세팅을 했죠.
@types 패키지들을 깔고 나면, 기존 코드에서 import 부분부터 에러가 납니다.
import React, { PureComponent } from 'react';
위에서 React가 default export가 아니라고 에러가 나죠. 수정하려면 다음과 같이 해야 합니다.
import * as React from 'react';
import { PureComponent } from 'react';
뭔가 두 줄이 돼서 마음에 들지 않았습니다. tsconfig.json 옵션에서 esModuleInterop이라는 옵션을 켜면 위처럼 한 줄로 쓸 수는 있습니다. 문법적으로 * as React를 쓰는 것이 올바른 방법이 맞다고 생각하고는 있지만 편의를 위해서 esModuleInterop을 켜서 사용합니다. es 모듈 강좌
잘 생각해보면 아래와 같은 경우는 React가 export default 되어 있을 때나 가능한 방식입니다.
import React, { PureComponent } from 'react';
하지만 React는 export default React가 아니라 그냥 export = React 되어 있습니다. 따라서 default가 없기 때문에 개별 export들을 모아주는 * as React가 맞다고 판단하긴 했습니다.
axios같은 경우는 타입 정의에 export default Axios로 되어 있기 때문에 아래처럼 하면 됩니다.
import axios from 'axios'; // default 가져오기
가끔가다 @types가 없거나 정의가 틀린(이 경우가 더 화가 납니다, connect-flash같은...) 패키지가 생깁니다. 타입스크립트도 많은 시행착오를 거치면서 언어가 만들어졌기 때문에 과거의 코드가 남아있어 사람들 골치를 썩입니다. 예를 들어 ES6 모듈 대신에 네임스페이스를 써서 글로벌 환경을 오염시킨다거나, 모듈 선언을 export = e 같은 것으로 했거나요.
어찌됐든 정의가 없는 모듈은 정의(.d.ts 파일)를 만들어주어야 합니다. 처음에는 정의가 없어도 에러가 안 나서 귀찮게 왜 만드냐고 하실 수 있는데 나중에 noImplicAny 옵션을 켜면 에러가 뿜뿜합니다.
tsconfig.json
{
"typeRoots": [
"./types",
"./node_modules/@types"
]
}
지난 시간의 tsconfig.json엣 typeRoots가 타입을 저장할 폴더를 의미합니다. 제가 만든 커스텀 타입들은 types에 넣어줍니다.
types/
index.d.ts
can-use-dom.d.ts
react-filepicker.d.ts
filestack-react.d.ts
express-http-proxy.d.ts
connect-flash.d.ts
types 폴더 내부 d.ts 파일들입니다. 위 패키지들은 타입이 없거나 틀려서 제가 임의로 만들었습니다. 몇 개 파일을 살펴보죠
index.d.ts
import { } from 'express';
declare global {
interface Window {
swUpdate: boolean;
Notification: any;
adsbygoogle: any[];
__INITIAL_STATE__: string;
Kakao: any;
FB: any;
google: any;
devToolsExtension(): () => void;
}
interface Error {
code?: number;
}
}
제가 임의로 만든 정의를 모아둔 파일입니다. declare global로 기존에 정의되어있던 인터페이스를 확장했습니다. 뭔가 전역 객체를 확장해서 꺼림칙하긴 하지만 딱히 다른 방법이 떠오르지 않네요. 개별적으로 extend하긴 너무 지저분하고요. import {} from 'express'는 파일을 모듈로 만들기 위한 꼼수입니다. 타입스크립트의 경우 import나 export 선언이 위에 없는 경우 일반 스크립트로 칩니다.
connect-flash.d.ts
declare module 'connect-flash' {
global {
namespace Express {
interface Request {
flash(): { [key: string]: string[] };
flash(message: string): any;
flash(event: string, message: string): void;
}
}
}
import express = require('express');
function flash(): express.RequestHandler;
export default flash;
}
이건 connect-flash를 제가 다시 타입 정의한 파일입니다. declare module '패키지명'으로 하시면 됩니다. 꼭 패키지를 100% 정확하게 정의할 필요는 없습니다. 그냥 자신이 쓰는 API 정도만 정의하시면 됩니다. 위의 경우처럼 다른 패키지의 객체를 확장해야 하는 경우가 있어 조금 헤맸습니다.
이렇게 정의한 타입들이 너무 훌륭하다고 생각하면 @types에 배포하시면 됩니다. 저는 passport-kakao와 passport-naver를 수정해서 배포했고, react-fb-like와 react-vote에 타입을 추가해서 배포했습니다. 타입스크립트를 하면서 오픈소스 컨트리뷰터가 되는 1석2조!
참고로 이미 ts로 만들어진 라이브러리인 경우, 굳이 .d.ts를 따로 만들 필요가 없습니다. 만들었다간 괜히 .d.ts랑 .ts 파일이 충돌납니다.
이제 나머지 파일들은 개별적으로 interface나 type을 정의하면 됩니다. 저는 리액트 환경에서 진행해서 리액트 타입을 분석하느라 골머리를 좀 썩혔습니다. 다음 포스팅에서 이어집니다.