간단하게 Foo
와 Bar
라는 엔티티 클래스를 정의했다.
// Bar.ts
interface BarField {
id: number;
}
export interface IBar {
readonly _: BarField;
get getId(): BarField["id"];
}
export class Bar implements IBar {
constructor(public _: BarField) {}
get getId() {
return this._.id;
}
}
BarField
: 생성자 함수를 위한 타입IBar
: 클래스의 필드와 각종 메소드를 위한 타입// Foo.ts
import { Bar, IBar } from "./Bar";
interface FooField {
id: string;
barList: Bar[]; // import 한 클래스 Bar 를 타입으로서 사용했다.
}
interface IFoo {
readonly _: FooField;
get getFirstBarId(): IBar["getId"] | undefined;
}
export class Foo implements IFoo {
constructor(public _: FooField) {}
get getFirstBarId() {
const firstFoo = this._.barList[0];
if (firstFoo) {
return parseInt(firstFoo.getId);
}
}
}
FooField
: 생성자 함수를 위한 타입IFoo
: 클래스의 필드와 각종 메소드를 위한 타입Foo
는 this.barList
라는 속성을 가지며, 타입은 Bar[]
이다. (Bar
클래스를 가져와서 타입으로 지정했다.)
이 경우, getFirstBarId
에서 praseInt(...)
부분에서 타입 에러가 발생하는 것을 확인할 수 있다.
이제 각종 타입 선언문을 d.ts
파일로 옮겨보자.
// @types/Entity/index.d.ts
declare namespace Entity {
namespace FooBar {
import { Bar } from "../../src/Bar";
interface FooField {
id: string;
barList: Bar[];
}
interface IFoo {
readonly _: FooField;
get getFirstBarId(): IBar["getId"] | undefined;
}
interface BarField {
id: number;
}
export interface IBar {
readonly _: BarField;
get getId(): BarField["id"];
}
}
}
그리고 기존 Foo
와 Bar
파일을 수정해주자.
// Bar.ts
export class Bar implements Entity.FooBar.IBar {
constructor(public _: Entity.FooBar.BarField) {}
get getId() {
return this._.id;
}
}
// Foo.ts
export class Foo implements Entity.FooBar.IFoo {
constructor(public _: Entity.FooBar.FooField) {}
get getFirstBarId() {
const firstFoo = this._.barList[0];
if (firstFoo) {
return parseInt(firstFoo.getId);
}
}
}
그런데 신기하게도 에러가 발생했던 부분에서 에러가 사라졌다.
정확한 이유는 파악하지 못했지만, 아무래도 d.ts
파일에서 non-primitive 타입은 추론이 제대로 동작하지 않게 되어버린 듯 하다. (다만 IDE에서 제공하는 타입 추론은 잘 작동한다.)
잘 작동하는 타입들은 primitive type 들이랑 interface
, type
으로 선언한 타입 뿐인 듯 하다. (abstract class
같은 것도 선언은 가능하지만 추론이 제대로 동작하지 않는다.)
그래서 index.d.ts
에서 Bar[]
대신 IBar[]
를 사용하도록 바꾸었다.
declare namespace Entity {
namespace FooBar {
// import { Bar } from "../../src/Bar";
interface FooField {
id: string;
// barList: Bar[];
barList: IBar[];
}
interface IFoo {
readonly _: FooField;
get getFirstBarId(): IBar["getId"] | undefined;
}
interface BarField {
id: number;
}
export interface IBar {
readonly _: BarField;
get getId(): BarField["id"];
}
}
}
그랬더니 다시 parseInt
부분에 타입 에러가 생겼다.
스택오버플로우의 Import class in definition file (*d.ts) 글을 보니, 다음과 같은 내용이 써져 있었다.
타입 선언 모듈이 2가지 종류가 있다고 하네요.
- 흔히
ts
파일 내에 작성하는 지역 모듈d.ts
파일 내에 작성하는 전역 모듈전역 모듈은 다른 이름으로 ambient module 이라고 하고, 이녀석은 지역 모듈에 이미 정의되어 있는 타입들에 병합될 수 있다고 합니다.
그리고 ambient module 은 inport 구문이 없어야 한다고 하네요. 그래서 제가
Bar
클래스를 import 한 시점에서 전역 모듈이 아니게 되어서 잘 작동을 안했던 것 같아요.그래서 사실 import를 쓰지 않는 방법(제가 공유드렸던 아티클의 해결방법)이 유일한 해결책이었는데, 2.9버전 부터는 동적 import가 가능해졋다는 것 같아요 ㅎㅎ
결론은, 해당 전역 모듈이 지역 모듈화가 되어 버려, symbolic link 를 제대로 찾지 못해, 타입 추론이 제대로 이루어지지 않은 것이 원인인 듯 하다.
실제로 현업 코드에서 동적 import 를 이용해 클래스 자체를 가져와서 타입으로 사용해본 결과, 타입 추론이 잘 작동했다!
위 스택 오버 플로우 링크에 나와있듯이, 동적 가져오기(dynamic import)를 사용하면 해결할 수 있습니다.
declare namespace Entity {
namespace FooBar {
interface FooField {
id: string;
// barList: IBar[];
barList: import(".../Bar").Bar[]; // dynamic import
}
/* interface IFoo {
readonly _: FooField;
get getFirstBarId(): IBar["getId"] | undefined;
} */
interface BarField {
id: number;
}
/* export interface IBar {
readonly _: BarField;
get getId(): BarField["id"];
}/*
}
}
불필요한 interface
를 제거하고, 클래스 자체를 동적 가져오기를 통해 타입으로서 사용해주면 정상적으로 작동한다.