본문 바로가기

javascript/typescript

[typescript] ts2345

 클라우드 강의 마지막 발표를 위해 기존에 aws 기반으로 구성한 뉴스 댓글 수집 파이프라인을 azure 기반으로 마이그레이션 하고 있다. 이 과정에서 뉴스 데이터를 수집할 때 타입 선언으로 정의한 내용이 있어 인터페이스로 변경해보았는데, 다음과 같은 에러가 발생했다.

// 인터페이스로 변경한 타입
interface NLListOptions {
  query: string;
  ds: string;
  de: string;
  news_office_checked?: string;
  office_type?: string;
  mynews?: string;
};

// 해당 객체를 이용하는 함수
export async function getNewsLinkList(
  variable_options: NLListOptions,
  delayOptions: DelayOptions = {},
): Promise<string[]> {
    // ...
    // 에러 발생 위치
     const baseUrl = getBaseUrl(path, options, variable_options);
    // ...
}

// 에러가 발생하는 함수의 파라미터
export function getBaseUrl(
  path: string,
  fixed_options: Record<string, string>,
  variable_options?: Record<string, string>,
) {//...}

발생한 에러

타입을 단지 인터페이스로 변경한 것 뿐인데, 왜 에러가 발생하는걸까 싶어 내용을 검색해보았다.

https://stackoverflow.com/questions/73003256/why-interface-produces-ts2345-but-type-not

 결론만 말하면 타입을 인터페이스로 변경할 때 에러가 발생하는 동작은 의도된 것으로, 주 원인은 인터페이스의 특징인 선언 병합 때문이다.

선언 병합 설명: https://www.typescriptlang.org/ko/docs/handbook/declaration-merging.html

선언 병합은 타입스크립트의 인터페이스가 가지는 특별한 성질로, 동일한 이름의 인터페이스를 여러 개 선언하면 선언된 프로퍼티가 모두 병합되어 하나의 인터페이스로 취급된다. 이 성질을 이용하면 express, express-session 등 여러 라이브러리가 가진 고유 객체를 확장하여 사용할 수 있게 된다. 미들웨어에서 request 객체에 유저가 정의한 객체를 삽입하는 등의 동작은 선언 병합 덕분에 가능하다.

/ express request 확장

//1. import로 User 가져오는 방식
import { User } from "./src/models/user";

declare global {
    namespace Express {
        export interface Request {
            user: User;
        }
    }
}

//2. import 구문 사용
//https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#import-types
declare namespace Express {
    type User = import('../../model/user.js').UserEntity
    interface Request {
        user?: User
        // user?: User
    }
}

이때, 문제가 되는 NLListOptions는 현재 시점에는 key, value 모두 string 타입으로 정의되어 있으나, 선언 병합에 의해 value의 타입이 string이 아닌 구조를 가질 수 있다. 이렇게 되면 NLListOptions 타입이 Record<string, string>을 만족할 수 없게 되므로, 안전한 타입을 위해 인터페이스로 정의된 경우 ts2345 에러를 내뿜고 있던 것이다.

 해결책은 매우 간단하다. interface을 type으로 변경한다. type은 선언 병합을 수행하지 않으므로, 한번 타입이 지정되면 항상 보장된다고 볼 수 있다.

(좌) 인터페이스 선언 병합, (우) 타입은 동일한 이름으로 여러 개 선언 불가능


 객체를 사용할 때는 인터페이스를 사용하는 것을 선호했지만, 선언 병합이라는 특징에 의해 문제가 발생할 수도 있다는 사실을 알게 되었다. 적절한 위치에 타입과 인터페이스를 사용할 수 있는 안목을 늘려야겠다.