Thiery Michel - 2025년 12월 4일
TypeScript가 튜링 완전(Turing complete)하다는 사실을 알고 계셨나요? 이 포스트에서 저는 타입 정의를 하나의 프로그램을 작성하는 것처럼 접근해 보려 합니다.
목표는 TypeScript로 Doom을 구현하거나 수학 연산을 수행하는 것이 아닙니다. 그런 것들은 이미 누군가 해냈습니다:
이러한 시도들은 인상적이지만, "왜?"라는 의문을 갖게 합니다.
우리의 목표는 타입을 프로그램처럼 다룸으로써 타입을 더 잘 작성하는 능력을 갖추는 것입니다.
제네릭 타입(Generic Types): 함수
TypeScript에서는 다른 타입에 의존하는 타입을 정의할 수 있습니다. 이는 함수처럼 작동합니다. 즉, 주어진 입력 타입에 대해 출력 타입을 생성합니다.
제네릭 타입 함수에 타입 제약 조건 추가하기
extends 키워드는 함수 인자에 타입을 지정하는 것처럼, 타입 매개변수가 특정 타입을 확장해야 함을 TypeScript에 알려줍니다.
제네릭 타입 함수에 기본 타입 추가하기
함수 매개변수와 마찬가지로 제네릭 타입 함수에도 기본 타입을 제공할 수 있습니다.
실무 예제: 간단한 CRUD 생성기
이것만으로도 이미 제네릭 CRUD 타입 생성기를 만들 수 있습니다.
조건문: 조건부 타입(Conditional Types)
함수가 있다면 조건문도 가질 수 있을까요?
네, 가능합니다. 타입 함수에서 extends 키워드를 사용하여 조건을 만들 수 있습니다.
extends는 한 타입이 다른 타입의 일부인지 테스트합니다. 그런 다음 삼항 연산자 구문인 condition ? trueCase : falseCase를 사용합니다.
실무 예제:
이벤트에서 이벤트 타입 추출하기
infer 키워드: 변수
infer 키워드는 타입 정의 내부에 변수를 생성합니다. 이는 구조 분해 할당(Destructuring)을 지원합니다.
실무 예제
infer를 사용하면 조건부 타입 섹션의 이벤트 예제를 다음과 같이 단순화할 수 있습니다.
재귀 타입(Recursive Types): 루프를 도는 방법
타입은 자기 자신을 호출할 수 있으며, 이를 통해 재귀 타입을 만들 수 있습니다.
재귀를 사용하여 트리와 같은 재귀적 데이터 구조의 타입을 정의할 수 있습니다.
또한 재귀를 사용하여 배열을 반복(Iterate)할 수도 있습니다.
배열에서 특정 타입을 검색하는 Find 타입을 구현해 보겠습니다. 찾으면 해당 타입을 반환하고, 그렇지 않으면 never를 반환합니다.
실무 예제: 미들웨어 결과 타입 정의하기
이것이 언제 유용할지 궁금할 수 있습니다.
미들웨어가 작업 인자에 따라 작업 결과를 수정하는 미들웨어 시스템을 상상해 보십시오.
미들웨어 타입을 알고 있고 특정 인자에 대한 결과 타입을 찾고 싶을 때 다음과 같이 할 수 있습니다.
문자열 조작: 템플릿 리터럴 타입(Template Literal Types)
TypeScript를 사용하면 템플릿 리터럴을 사용하여 타입 레벨에서 문자열을 다룰 수 있습니다.
단순한 문자열 결합을 수행할 수 있습니다.
또한 infer 키워드와 재귀를 사용하여 문자열에서 모든 공백을 제거하는 것과 같은 더 복잡한 문자열 조작도 수행할 수 있습니다.
실무 예제: 속성 이름으로부터 Getter 메서드 이름 생성하기
참고: Capitalize는 문자열 타입의 첫 글자를 대문자로 바꿔주는 내장 유틸리티 타입입니다.
Uppercase와 재귀를 사용하여 직접 구현할 수도 있습니다.
맵드 타입(Mapped Types)
재귀를 통해 배열을 루프 돌 수 있었지만, 객체 속성은 어떻게 반복할까요?
맵드 타입을 사용하면 기존 타입의 각 속성을 변환하여 새로운 타입을 만들 수 있습니다.
맵드 타입은 인덱스 접근 타입(Indexed Access Types) 구문과 keyof 연산자를 기반으로 합니다.
인덱스 접근 타입
인덱스 접근 타입을 사용하면 Type[Key] 구문을 통해 객체 타입의 속성 타입을 가져올 수 있습니다.
또한 키들의 유니온(Union)을 사용하여 해당 키들의 타입 유니온을 가져올 수도 있습니다.
keyof 연산자
keyof 연산자를 사용하면 타입의 모든 키에 대한 유니온을 가져올 수 있습니다.
in 키워드
마지막으로 in 키워드를 추가하여 타입의 모든 키를 반복할 수 있습니다.
이를 통해 객체의 모든 메서드가 Promise를 반환하도록 변환하는 타입을 만들 수 있습니다.
실무 예제
다음은 맵드 타입의 더 복잡한 예제입니다.
리소스 타입과 필터 키 목록을 받아 각 필터 키에 대한 getByFilterType 메서드를 생성해 보겠습니다.
함수 객체를 단일 함수로 축소하기
keyof를 사용하면 객체의 형태를 기반으로 객체뿐만 아니라 무엇이든 반환할 수 있습니다.
예를 들어, 여기 함수 객체를 단일 함수로 변환하는 함수가 있습니다. 이 함수는 객체의 키를 첫 번째 인자로 받고, 함수의 인자들을 나머지 인자로 받습니다.
참고: 이전 예제에서는 TypeScript에서 제공하는 유틸리티 타입인 ReturnType과 Parameters를 사용했습니다.
ReturnType은 주어진 함수 타입이 반환하는 값의 타입을 반환합니다.
궁금하시다면 다음과 같이 직접 구현할 수 있습니다:
Parameters는 주어진 함수 타입의 인자들을 반환합니다.
다음과 같이 직접 구현할 수 있습니다:
결론
TypeScript 타입 정의를 프로그래밍 언어로 다룸으로써 고급 제네릭 타입을 정의할 수 있습니다.
이러한 제네릭 타입은 결과적으로 더 제네릭한 코드를 가능하게 하여, 코드베이스 전반에서 중복을 줄이고 타입 안전성을 향상시킵니다.
핵심 요점은 다음과 같습니다:
- 조건부 타입(
extends ? :)은 분기 로직을 가능하게 합니다.
infer 키워드는 변수 할당처럼 작동하며 구조 분해처럼 다른 타입에서 값을 추출할 수 있게 해줍니다.
- 재귀는 배열과 복잡한 타입에 대한 반복을 가능하게 합니다.
- 템플릿 리터럴은 타입 레벨에서 문자열 조작을 가능하게 합니다.
- 맵드 타입은 객체 속성에 대한 반복을 제공합니다.
이러한 도구들을 사용하면 필요에 따라 적응하고, 컴파일 타임에 오류를 잡아내며, IDE에서 훌륭한 자동 완성을 제공하는 정교한 타입 유틸리티를 만들 수 있습니다. 다음에 타입 정의를 작성할 때는 프로그램을 작성한다고 생각하십시오. 실제로 여러분은 바로 그 일을 하고 있는 것이기 때문입니다.