Typescript Infer 4편 : PascalCase + Underscore 변환기 만들기

프디·2025년 4월 13일

Typescript infer

목록 보기
4/5

✅ 우리가 다루는 대상

이번 문제의 목표는 문자열을 타입 수준에서 다음처럼 변환하는 것이다:

"hello  world   typescript""Hello__World___Typescript"
"hello world typescript""Hello_World_Typescript"

즉:

  1. 각 단어의 첫 글자는 대문자로 변환 (PascalCase)
  2. 공백은 _로 변환하되, 공백 개수에 따라 _ 개수도 반영
  3. 타입 레벨에서 이 작업을 구현

🎯 문제 정의

type PascalCaseWithUnderscore<T extends string> = ???;

예시:

type Test1 = PascalCaseWithUnderscore<'hello world typescript'>;   // "Hello_World_Typescript"
type Test2 = PascalCaseWithUnderscore<'this  is a test'>;          // "This__Is_A_Test"
type Test3 = PascalCaseWithUnderscore<'  spaced start'>;           // "__Spaced_Start"
type Test4 = PascalCaseWithUnderscore<'single'>;                   // "Single"
type Test5 = PascalCaseWithUnderscore<'hello   world'>;            // "Hello___World"

🔧 구현

1. 앞 공백 제거 유틸리티

문자열 맨 앞의 공백이 _로 처리되지 않도록, 진입 시 한 번만 제거합니다.

type RemoveLeadingSpaces<T extends string> =
  T extends ` ${infer Rest}` ? RemoveLeadingSpaces<Rest> : T;

2. 내부 PascalCase + Underscore 처리

이제 문자열을 ' ' 기준으로 분해하며, 각 단어의 첫 글자를 대문자로 바꾸고 _로 연결합니다.

type InnerPascal<T extends string> =
  T extends `${infer Word} ${infer Rest}`
    ? `${Capitalize<Word>}_${InnerPascal<Rest>}`
    : Capitalize<T>;
  • Word가 빈 문자열("")인 경우 → _만 추가됨
  • 공백이 여러 개면 _가 그만큼 반복됨

3. 최종 타입

사용자에게는 이 타입만 보이도록 래핑합니다:

type PascalCaseWithUnderscore<T extends string> =
  InnerPascal<RemoveLeadingSpaces<T>>;

✅ 테스트

type T1 = PascalCaseWithUnderscore<'hello world typescript'>;
// "Hello_World_Typescript"

type T2 = PascalCaseWithUnderscore<'this  is a test'>;
// "This__Is_A_Test"

type T3 = PascalCaseWithUnderscore<'  spaced start'>;
// "__Spaced_Start"

type T4 = PascalCaseWithUnderscore<'single'>;
// "Single"

type T5 = PascalCaseWithUnderscore<'hello   world'>;
// "Hello___World"

✅ 런타임에서도 확인해보기

function pascalCaseWithUnderscore<T extends string>(
  input: T
): PascalCaseWithUnderscore<T> {
  return input
    .split(' ')
    .map(word =>
      word ? word[0].toUpperCase() + word.slice(1) : ''
    )
    .join('_') as PascalCaseWithUnderscore<T>;
}

console.log(pascalCaseWithUnderscore('hello  world   typescript'));
// "Hello__World___Typescript"

console.log(pascalCaseWithUnderscore('   spaced start'));
// "__Spaced_Start"

💡 어떻게 작동하는가?

단계설명
RemoveLeadingSpaces맨 앞의 공백을 모두 제거 " hello""hello"
InnerPascal' ' 기준으로 분해하고, 빈 문자열도 _로 처리
Capitalize<Word>단어의 첫 글자를 대문자로 변환

🔁 공백이 _로 바뀌는 정확한 흐름

입력:

'hello   world'
type PascalCaseWithUnderscore<'hello   world'>
→ InnerPascal<'hello   world'>

재귀 흐름:

  • 1회 호출:

    Word = "hello"
    Rest = "  world""Hello_" + InnerPascal<"  world">
  • 2회 호출:

    Word = ""
    Rest = " world""Hello__" + InnerPascal<" world">
  • 3회 호출:

    Word = ""
    Rest = "world""Hello___" + InnerPascal<"world">
  • 4회 호출:

    Word = "world"
    → Capitalize<"world"> = "World"

✅ 최종 결과:

"Hello___World"

🔍 빈 문자열이 _가 되는 원리

type PascalCaseWithUnderscore<'hello   world'>
→ InnerPascal<'hello   world'>
→ Word = "hello", Rest = "  world"
→ InnerPascal<'  world'> → Word = "", Rest = " world"
→ InnerPascal<' world'> → Word = "", Rest = "world"
→ InnerPascal<'world'> → Word = "world" ()"Hello___World"

✅ 타입 일치 여부 확인하기

type AssertEqual<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2) ? true : false;

type Check = AssertEqual<
  PascalCaseWithUnderscore<'hello   world'>,
  "Hello___World"
>; // ✅ true

📌 최종 타입 구조 요약

// 1. 공백 제거
type RemoveLeadingSpaces<T extends string> =
  T extends ` ${infer Rest}` ? RemoveLeadingSpaces<Rest> : T;

// 2. 내부 로직
type InnerPascal<T extends string> =
  T extends `${infer Word} ${infer Rest}`
    ? `${Capitalize<Word>}_${InnerPascal<Rest>}`
    : Capitalize<T>;

// 3. 최종 타입
type PascalCaseWithUnderscore<T extends string> =
  InnerPascal<RemoveLeadingSpaces<T>>;

profile
프론트엔드개발자인디

0개의 댓글