본문 바로가기
4주차

TypeScript에서 같은 이름의 타입을 여러 개 생성하면 어떻게 될까?

by LOCOCO 팀블로그 2025. 5. 13.

안녕하세요 SOPT 36기 WEB파트 YB 김정은입니다!

 

타입스크립트를 사용하다 보면 타입 선언을 많이 하게 되는데요, 선언 타입의 이름을 지어주는데 동일한 이름이 이미 존재하면 어떻게 될지 궁금하지 않으신가요? 저는 궁금해서 찾아봤는데요... 중복된 변수명이 있으면 에러가 발생하는 것과 동일하게 에러가 발생할 것이라고 생각했는데 아니었습니다...🫢

오늘은 이 내용에 대해 글을 작성해보고자 해요.

 

타입스크립트를 사용해보신 분들이라면 다들 타입 선언을 해보셨을 거예요.

interface Info {
  name: string;
  age: number;
}

이런 식으로 인터페이스를 선언할 수 있는데요, 여기서 Info가 하나 더 생긴다면 어떻게 될까요?

 

interface Info {
  name: string;
  age: number;
}

interface Info {
  gender: "male" | "female";
}

const person: Info = { name: "정은", age: 23, gender: "female" };

이렇게 같은 이름인 타입이 여러 개여도 에러가 발생하지 않아요.

 

Info는 자동으로 병합되어서 아래처럼 합쳐지게 돼요.

interface Info {
  name: string;
  age: number;
  gender: "male" | "female";
}

 

위와 같은 상황을 선언병합이라 하고 타입스크립트의 기능 중 하나예요.

 

선언병합 (Declaration Merging)

선언병합은 타입스크립트 컴파일러가 동일한 이름을 가진 두 개 이상의 독립적인 선언을 하나로 결합하는 것을 말해요.

 

선언 병합이 발생하는 케이스는 다양해요.

  • 인터페이스 - 인터페이스
  • 네임스페이스 - 네임스페이스
  • 네임스페이스 - 클래스
  • 네임스페이스 - 함수
  • 네임스페이스 - Enum
  • 모듈 보강 및 전역 보강

위의 경우에 대해서 하나씩 알아볼게요.

 

인터페이스 병합

같은 이름을 가진 인터페이스가 여러 번 선언되면 하나의 인터페이스로 병합돼요. 가장 기본적인 경우는 위에서 확인한 코드와 같은 경우예요.

interface Info {
  name: string;
  age: number;
}

interface Info {
  gender: "male" | "female";
}

const person: Info = { name: "정은", age: 23, gender: "female" };​

 

하지만 중복된 속성이 있지만 타입이 동일하지 않다면 컴파일 에러가 발생해요.

// 병합됨
interface Info {
  name: string;
  age: number;
}

interface Info {
  name: string;
  gender: "male" | "female";
}

// age의 타입이 동일하지 않으므로 컴파일 오류 발생
interface Info {
  name: string;
  age: number;
}

interface Info {
  age: string;
  gender: "male" | "female";
}

 

함수 오버로드

동일한 이름의 메서드가 여러 인터페이스 내에 선언되어 있다면 함수 오버로드로 처리돼요. 나중에 선언된 인터페이스 내의 함수가 더 높은 우선순위를 갖게 돼요.

interface Mail {
  sendMail(content: string): void;
}

interface Mail {
  sendMail(content: string, to: string): void;
}

// 병합
// interface Mail {
//   sendMail(content: string, to: string): void;
//   sendMail(content: string): void;
// }

 

제너릭 파라미터

모든 선언에서 동일한 타입 제약 조건을 가져야 병합될 수 있어요.

// 조건이 달라서 에러 발생
interface Box<T> {
  value: T;
}

interface Box<T extends number> { 
  id: string;
}

// 병합 가능:
interface Box<T> {
  value: T;
}

interface Box<T> {
  id: string;
}

 

네임스페이스 병합

 

인터페이스와 동일한 병합 방식을 가져요

namespace Validation {
  export interface StringValidation {
    isValid(s: string): boolean;
  }
}

namespace Validation {
  export interface NumberValidation {
    isValidNumber(n: number): boolean;
  }
}

// 병합:
// namespace Validation {
//   export interface StringValidator {
//     isValid(s: string): boolean;
//   }
//   export interface NumberValidator {
//     isValidNumber(n: number): boolean;
//   }
// }

 

하지만 export 되지 않은 멤버의 경우는 병합에서 제외돼요.

namespace Animal {
  const haveMuscles = true;
  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}

namespace Animal {
  const doAllHaveMuscles = false;
  export function doAnimalsHaveMuscles() {
    // return haveMuscles; // haveMuscles는 export 되지 않아 병합된 네임스페이스에서 접근 불가로 에러
    return doAllHaveMuscles;
  }
}

 

네임스페이스  - 클래스 병합

 

클래스와 동일한 이름의 네임스페이스가 선언되면 네임 스페이스는 클래스의 static side를 확장해요. 내부 클래스와 정적 메서드를 혼합할 수 있어요.

클래스의 유틸리티 함수나 추가 타입 정의를 클래스와 묶을 수 있게 도와줘요.

class Album {
  label: Album.AlbumLabel;
  
  constructor(label: Album.AlbumLabel) {
    this.label = label;
  }
}

namespace Album {
  export interface AlbumLabel {
    name: string;
    founded: number;
  }
  
  export function isLegendary(album: Album): boolean {
    return album.label.founded < 1970;
  }
}

const jazzLabel: Album.AlbumLabel = { 
  name: "Blue Note", 
  founded: 1939 
};

const blueTrainAlbum = new Album(jazzLabel);
const isClassic = Album.isLegendary(blueTrainAlbum);

 

네임스페이스 - 함수

네임스페이스와 함수를 결합하면 함수에 속성을 추가할 수 있어요.

function makeText(name: string): string {
  return makeText.prefix + name + makeText.suffix;
}

namespace makeText {
  export let suffix = "";
  export let prefix = "Hello, ";
  
  export interface Styling {
    fontWeight?: string;
    fontSize?: number;
  }
  
  export function getDefaultStyling(): Styling {
    return { fontWeight: "bold", fontSize: 14 };
  }
}

console.log(makeText("SOPT"));  // "Hello, SOPT"

makeText.prefix = "안녕하세요, ";
console.log(makeText("SOPT"));  // "안녕하세요, SOPT"

const styling = makeText.getDefaultStyling();
console.log(styling);  // { fontWeight: "bold", fontSize: 14 }

 

네임스페이스 - enum 병합

enum과 네임스페이스를 병합하면 enum의 멤버를 네임스페이스에서 확장할 수 있어요

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}

namespace Color {
  export function mixColors(a: Color, b: Color): Color {
    return a | b;
  }
  
  export function isGreen(color: Color): boolean {
    return (color & Color.Green) !== 0;
  }
}

let purple = Color.mixColors(Color.Red, Color.Blue);  // 5
let isItRed = Color.isGreen(purple);  // true

 

모듈 보강과 전역 보강

기존 모듈에 새로운 멤버를 추가하고 싶다면 모듈 보강을 사용할 수 있어요.

declare module 'react' {
  interface HTMLAttributes<T> {
    customAttr?: string;
  }
}

const element = <div customAttr="value">모듈보강</div>;

 

전역 스코프 선언을 확장하고 싶다면 전역 보강을 사용해요.

export {};

declare global {
  interface String {
    padZero(length: number): string;
  }
}

String.prototype.padZero = function(length) {
  let s = String(this);
  while (s.length < length) {
    s = '0' + s;
  }
  return s;
};

console.log("123".padZero(5));  // "00123"

 


 

타입스크립트 선언 병합은 코드를 유연하게 사용할 수 있게 해 주지만 선언 병합을 남용하면 코드가 복잡해지고 타입 안정성에 안 좋은 영향을 미칠 수도 있기 때문에 조심해서 사용하는 것이 좋아요 👀