이번 문제의 목표는 문자열을 타입 수준에서 다음처럼 변환하는 것이다:
"hello world typescript"
→ "Hello__World___Typescript"
"hello world typescript"
→ "Hello_World_Typescript"
즉:
_로 변환하되, 공백 개수에 따라 _ 개수도 반영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"
문자열 맨 앞의 공백이 _로 처리되지 않도록, 진입 시 한 번만 제거합니다.
type RemoveLeadingSpaces<T extends string> =
T extends ` ${infer Rest}` ? RemoveLeadingSpaces<Rest> : T;
이제 문자열을 ' ' 기준으로 분해하며, 각 단어의 첫 글자를 대문자로 바꾸고 _로 연결합니다.
type InnerPascal<T extends string> =
T extends `${infer Word} ${infer Rest}`
? `${Capitalize<Word>}_${InnerPascal<Rest>}`
: Capitalize<T>;
Word가 빈 문자열("")인 경우 → _만 추가됨_가 그만큼 반복됨사용자에게는 이 타입만 보이도록 래핑합니다:
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>>;