NestJS 프로젝트에서 Sequelize 쿼리를 작성하는 중 문제가 발생했다.
service 에 다음과 같은 메소드가 있을 때,
async findAll(): Promise<User[]> {
return await this.userModel.findAll({
attributes: [
'id',
[sequelize.col('tasks.id'), 'defaultTaskId'], // aliasing
],
include: [
{
model: Task,
attributes: [],
}
],
order: ['tasks', 'createdAt', 'ASC'],
});
}
sequelize 에서는 위와 같이 include 된 테이블의 컬럼의 이름을 alias 할 수 있다.
하지만 타입스크립트와 사용했을 때 다음과 같은 문제가 발생했다.
attributes: [
'id',
[sequelize.col('tasks.id'), 'defaultTaskId'], // aliasing
^^^^^^^^^^^^^^^^^^^^^^^^^^
// Type 'Col' is not assignable to type 'string | Literal | Fn'.
],
외부 모듈의 타입선언 관련 문제는 이전에도 한번 발생했다.
다음은 Controller의 한 메소드이다.
@Get()
async findAll(@Req() req: Request): Promise<Project[]> {
return await this.projectService.findAll(req.user);
^^^^^^^
// Argument of type 'Express.User' is not assignable to parameter of type 'import("/some/path/src/user/user.model").User'
}
passport 인증을 거친 후 컨트롤러에서 req.user 를 사용하려 했다. 하지만 node_modules/@types/passport/index.d.ts 를 보면 다음과 같다.
declare global {
namespace Express {
interface AuthInfo {}
interface User {}
interface Request {
authInfo?: AuthInfo;
user?: User;
// ... 생략
}
// ... 생략
}
}
req.user 는 빈 인터페이스인걸 알 수 있다. 위 선언은 기존 express 의 타입선언을 declaration merging 을 통해 확장하는 과정이다.
node_modules/@types/express-serve-static-core 를 보면 다음 부분이 포함되어있다.
declare global {
namespace Express {
// These open interfaces may be extended in an application-specific manner via declaration merging.
// See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
interface Request {}
interface Response {}
interface Application {}
}
}
따라서 나의 프로젝트에도 declaration 파일을 만들어 선언을 확장하면 된다.
src/types/index.d.ts 파일을 만들고,
import { User as UserModel } from '../user/user.model';
declare global {
namespace Express {
export interface User extends UserModel {}
}
}
위와 같이 Express 네임스페이스의 User 타입이 내 앱에서 선언한 User 모델을 확장하도록 하면 문제가 해결된다.
위에서 문제를 해결한것 처럼 src/types/index.d.ts 파일을 통해서 간단하게 해결할 수 있을줄 알았다. 하지만 Request 타입 문제는 application specific 하게 타입 확장을 할 수 있도록 일부러 글로벌에 namespace 를 통해 선언한 것이었고, 문제 1 은 그냥 sequelize 라이브러리의 타입 선언이 잘못된 것이었다.
잘못 선언된 부분을 찾아보니,
node_modules/sequelize/types/lib/model.d.ts 안의 다음 부분이었다.
export type ProjectionAlias = readonly [string | Literal | Fn, string];
^^^^
// string | Literal | Fn | Col 이어야 함
export type FindAttributeOptions =
| (string | ProjectionAlias)[]
| {
exclude: string[];
include?: (string | ProjectionAlias)[];
}
| {
exclude?: string[];
include: (string | ProjectionAlias)[];
};
위와 같이 alias 문법이 튜플 형태로 표현되어 있는데 Col 이 빠져있었다.
이를 해결하기 위해서 sequlize 의 types 디렉토리의 하위 파일들을
src/types/sequelize 디렉토리로 복사하고,
src/types/sequelize/lib/model.d.ts 의 잘못된 부분을 고쳐준다.
이후 sequelize 모듈의 resolution path 를 추가한다.
{
"compilerOptions": {
// ... 생략
"paths": {
"sequelize": ["src/types/sequelize"]
}
}
}
위와 같이 tsconfig.json 의 컴파일러 옵션을 바꿔주면 해결된다.
'popo' 라는 외부 모듈이 있고 다음과 같이 import 한다고 치자.
// ./src/test/t.s
import Test from 'popo'
Non-relative resolution 과정은 다음과 같다.
compilerOptions.paths 에 'popo' 에 대한 경로가 선언되어 있으면 그 경로를 탐색한다.
없다면 node_modules 폴더를 찾는다.
src/test/node_modules 탐색, 없으면
src/node_modules 탐색, 없으면
/node_modules 탐색
위와 같이 node_modules 를 찾을 때 까지 부모 디렉토리를 탐색하며 올라간다.
(baseUrl 옵션이 선언되어있다면 선언된 위치까지)
node_modules 폴더를 찾았다면 다음과 같은 순서로 탐색한다.
/some/path/node_modules/popo.ts
/some/path/node_modules/popo.tsx
/some/path/node_modules/popo.d.ts
/some/path/node_modules/popo/package.json (if it specifies a "types" property)
/some/path/node_modules/@types/popo.d.ts
/some/path/node_modules/popo/index.ts
/some/path/node_modules/popo/index.tsx
/some/path/node_modules/popo/index.d.ts
위와 같이 paths 에 지정된 경로를 최우선으로 찾는다. 그래서 모듈 선언을 대체하고 싶다면
"compilerOptions": {
"paths": {
"popo": ["your/new/declaration/path"]
}
}
위와 같이 해결하면 된다.
타입 선언을 Sequelize 에 반영하기 위해 이슈 템플릿에 맞춰 이슈 를 작성한다.
Sequelize 레포를 내 계정에 포크하고 브랜치를 생성한다. 코드 수정 후 커밋 컨벤션에 맞게 커밋 한다. Sequelize 는 앵귤러의 커밋 컨벤션을 따른다.
이후 PR 템플릿에 맞춰 PR 을 날린다.
한 일주일 후 머지 되고 버전 6.6.0 에 반영되었다.
이제 compilerOption.paths 를 생략하고 src/types/sequelize 디렉토리를 삭제해도 문제 없이 동작한다.
비록 티끌같은 커밋이지만 내가 자주 사용하는 라이브러리에 기여하는 과정이 꽤나 뿌듯하고 재미있었다.
https://www.typescriptlang.org/tsconfig
https://www.typescriptlang.org/docs/handbook
https://medium.com/naver-fe-platform/타입스크립트-컴파일러가-모듈-타입-선언을-참조하는-과정-5bfc55a88bb6