Nest.js 공식 문서를 보면 아래와 같은 내용이 있다.
constructor(private configService: ConfigService<{ PORT: number }, true>) {
const port = this.configService.get('PORT', { infer: true });
// The type of port will be 'number' thus you don't need TS type assertions anymore
}
주입받은 ConfigService
의 첫 번째 제네릭에 config의 타입을, 두 번째 제네릭에 true
를 명시하면, get
메소드를 통해 반환되는 타입에 undefined
을 제거할 수 있다.
기존에는 string | undefined
와 같이 유니온 타입을 반환했다.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (
configService: ConfigService<DatabaseConfig, true>,
): TypeOrmModuleOptions => {
return {
type: configService.get('DB_TYPE', { infer: true }),
host: configService.get('DB_HOST', { infer: true }),
port: configService.get<number>('DB_PORT'),
database: configService.get<string>('DB_DATABASE'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
entities: [],
synchronize: true, // set to false in production
};
},
inject: [ConfigModule],
}),
하지만 예상과는 다르게 return으로 반환한 객체의 type이 'postgres' | 'mysql' | 'aurora-mysql' | ... | undefined
로 추론되었고, useFactory 반환 타입과 호환되지 않았다.
머임? 왜 안되는데? 표정으로 멍청하게 쳐다보면서 안되는 이유를 찾아 나서게 되었다.
타입 스크립트에는 타입 추론이라는 메커니즘이 있다.
명시적으로 타입 어노테이션을 작성하지 않아도, 알아서 상황에 맞춰 타입을 추론해주는 시스템이다.
TypeScript는 다음과 같은 순서로 타입을 추론한다.
const num: number = 1;
위와 같이 명시적으로 number
타입을 어노테이팅 하면, num 변수는 number
타입이 된다.
const num = 1;
변수를 초기화 하게 되면 초기화 값을 가지고 타입을 추론하여, num 변수는 number
타입으로 추론된다.
constructor(private configService: ConfigService<{ PORT: number }, true>) {
const port = this.configService.get('PORT', { infer: true });
// The type of port will be 'number' thus you don't need TS type assertions anymore
}
공식 문서에서 나온 내용도 마찬가지다.
/**
* Get a configuration value (either custom configuration or process environment variable)
* based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
* @param propertyPath
*/
get<T = any>(propertyPath: KeyOf<K>): ValidatedResult<WasValidated, T>;
/**
* Get a configuration value (either custom configuration or process environment variable)
* based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
* @param propertyPath
* @param options
*/
get<T = K, P extends Path<T> = any, R = PathValue<T, P>>(propertyPath: P, options: ConfigGetOptions): ValidatedResult<WasValidated, R>;
configService의 get method의 내용이다.
다 필요없고 중요한 건, ConfigService의 두 번째 제네릭으로 true를 명시하면, undefined를 없앨 수 있다.
const type = configService.get('DB_TYPE', { infer: true });
const host = configService.get('DB_HOST', { infer: true });
이렇게 변수를 초기화 하면 type과 host 변수의 타입은 string
으로 잘 추론된다.
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (
configService: ConfigService<DatabaseConfig, true>,
): TypeOrmModuleOptions => {
return {
type: configService.get('DB_TYPE', { infer: true }),
host: configService.get('DB_HOST', { infer: true }),
port: configService.get<number>('DB_PORT'),
database: configService.get<string>('DB_DATABASE'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
entities: [],
synchronize: true, // set to false in production
};
},
inject: [ConfigModule],
}),
위와 같이 작성하면,
return 문의 리터럴 객체에 type
프로퍼티의 타입은 (property) type: "postgres" | "mysql" | "mariadb" | "cockroachdb" | "sqlite" | "mssql" | "sap" | "oracle" | "cordova" | "nativescript" | "react-native" | "sqljs" | "mongodb" | "aurora-mysql" | ... 5 more ... | undefined
이렇게 나온다.
왜..? 뭔데..?
반환타입 TypeOrmModuleOptions
가 목표 타입이다.
이렇게 목표 타입이 있는 경우, TypeScript가 양방향으로 타입 체크를 하게된다.
내 코드상 configService.get('DB_TYPE', { infer: true })
의 타입은 'postgres'
타입이다.
TypeOrmModuleOptions
의 type 프로퍼티가 기대하는 타입은 "postgres" | "mysql" | "mariadb" | "cockroachdb" | "sqlite" | "mssql" | "sap" | "oracle" | "cordova" | "nativescript" | "react-native" | "sqljs" | "mongodb" | "aurora-mysql" | ... 5 more ... | undefined
이다.
최종적으로 추론되는 타입은,
TypeScript가 더 넓은 타입으로 추론하게 되기 때문에, return문의 객체에서 type 프로퍼티 타입이 "postgres" | "mysql" | "mariadb" | "cockroachdb" | "sqlite" | "mssql" | "sap" | "oracle" | "cordova" | "nativescript" | "react-native" | "sqljs" | "mongodb" | "aurora-mysql" | ... 5 more ... | undefined
가 되는 것이다.
이건 TypeOrmModuleOptions
타입을 까보면 알 수 있다.
export type TypeOrmModuleOptions = {
retryAttempts?: number;
// ...
} & Partial<DataSourceOptions>;
export type DataSourceOptions =
| MysqlConnectionOptions
| PostgresConnectionOptions
| CockroachConnectionOptions
| ... (18개의 타입)
// 각각의 구체적인 타입
type PostgresConnectionOptions = {
type: "postgres"; // ← required (필수)
host?: string;
// ...
}
type MysqlConnectionOptions = {
type: "mysql"; // ← required (필수)
host?: string;
// ...
}
TypeOrmModuleOptions
에서 DataSourceOptions
를 Partial
타입으로 어노테이팅 했고, DataSourceOptions
타입은 Discriminated Union 타입이다.
유니온 타입을 Partial
타입으로 어노테이팅 함으로써 각 타입의 type
프로퍼티가 optional이 된다.
즉, Discriminated Union 타입에서 discriminator가 optional이 되어 discriminator 역할을 못하게 되고, TypeOrmModuleOptions
의 type
프로퍼티는 아래와 같이 평가된다.
type?: "postgres" | "mysql" | "mariadb" | ... | undefined
TypeOrmModuleOptions
의 타입은 아래와 같이 전개된다.
type TypeOrmModuleOptions =
| ({ retryAttempts?: ... } & Partial<PostgresConnectionOptions>) // type?: "postgres"
| ({ retryAttempts?: ... } & Partial<MysqlConnectionOptions>) // type?: "mysql"
| ({ retryAttempts?: ... } & Partial<AuroraMysqlConnectionOptions>) // type?: "aurora-mysql"
| ...
결국 type
프로퍼티의 타입과, TypeOrmModuleOptions
가 호환이 되지 않게 된다.
각 프로퍼티를 변수 할당을 먼저 하여 타입을 먼저 확정하면 된다.
useFactory: (
configService: ConfigService<DatabaseConfig, true>,
): TypeOrmModuleOptions => {
const type = configService.get('DB_TYPE', { infer: true });
const host = configService.get('DB_HOST', { infer: true });
const port = configService.get('DB_PORT', { infer: true });
const database = configService.get('DB_DATABASE', { infer: true });
const username = configService.get('DB_USERNAME', { infer: true });
const password = configService.get('DB_PASSWORD', { infer: true });
return {
type,
host,
port,
database,
username,
password,
entities: [],
synchronize: true,
};
}