icon

메티의 블로그

타입챌린지 스터디 후기와 타입스크립트 간단 정리
타입챌린지 스터디 후기와 타입스크립트 간단 정리

타입챌린지 스터디 후기와 타입스크립트 간단 정리

Tags
TypeScript
날짜
May 28, 2025
상태
공개

개요

전 직장 분들과 타입챌린지 스터디를 같이 하며, 문제를 풀고 서로 풀이를 공유하는 스터디를 진행했습니다. 타입챌린지 스터디를 통해 굳이 이렇게 까지 해야하나 싶은 문제까지 풀면서 생긴 생각들을 정리해보고자 합니다.

타입챌린지란?

type-challenges
type-challengesUpdated Jun 29, 2025
타입챌린지는 타입스크립트로 다양하게 조건에 맞는 타입을 만들어보는 챌린지 입니다. 마치 타입스크립트로 간단한 알고리즘 문제를 푸는 것이라고 보면 될 것 같습니다. 타입챌린지는 한국어도 어느정도 지원합니다. 그리고, 난이도별로 문제들을 분류 해 놓았으며, 번호 순서대로 나중에 나온 문제기 때문에, 번호가 높을수록 같은 난이도에서도 점점 어려워지는 경향이 있습니다. 그래서 난이도순, 그리고 동일 난이도 내에서는 번호순으로 문제를 풀이하니 점진적으로 학습이 어느정도 가능했습니다.
저는 warm-up easy medium 세가지 난이도에 해당하는 문제들을 1주에 평균 6문제씩 총 15주에 걸쳐(마지막 주차가 19주차라고 적혀있지만, 그것은 중간에 합류했기 때문) 116문제를 풀이 하였습니다. 타입챌린지는 레포지토리의 이슈 탭에 답안을 공유하고 있어서 이를 보며 더 좋은 답안을 찾거나 감조차 안 오는 문제에 실마리를 잡을 수도 있습니다.
레포지토리의 README 중간에 보시면, 어떻게 시작할지 다양하게 선택해 풀이를 할 수 있는데요. README 문서 안에 있는 문제 태그들을 클릭해 플레이그라운드에서 문제를 풀이 할 수도 있고, IDE나 코드에디터에 문제들을 받아서 로컬에서 풀이를 할 수도 있습니다.
README 파일에 친절하게 어떻게 시작할지 알려줍니다.
README 파일에 친절하게 어떻게 시작할지 알려줍니다.

후기

저는 타입스크립트를 그냥 타입 검사를 해주는 자바스크립트의 슈퍼셋 언어이며, 컴파일 과정이 필요한 특징이 있는 정도, 그리고 자주 쓰이는 유틸리티 타입 몇개 아는 정도로만 사용을 했었습니다.
하지만, 타입챌린지 스터디를 통해 타입스크립트가 타입을 어떻게 검사하고, 새로운 타입은 어떻게 만들어야하며, 타입스크립트가 추구하는 바가 무엇인지를 좀 더 깊게 체감해볼 수 있던 것 같습니다.
타입스크립트의 타입을 지원하는 라이브러리의 코드를 보면 이건 대체 무슨 타입이야? 싶었던 적이 있으시다면 꼭 한번 체험해보시는걸 추천드립니다. easy 난이도 정도의 문제와 medium 문제 몇 문제만 풀어도 충분히 될 것 같았어요. medium 의 모든 문제를 풀려니 풀었던 문제들을 반복하는 느낌도 받았습니다.

챌린지 후 타입스크립트 정리

타입챌린지 후 미리 알았더라면 유용했을 법 한 것들을 아래에 정리합니다.

타입스크립트의 목적

타입스크립트의 목적은 타입관련 검사를 해주지 않는 자바스크립트를 위해 타입 검사를 추가한 언어라고 생각하면 이해가 쉽습니다. 즉, 타입스크립트는 모든 자바스크립트의 기능을 다 가지고 있는 상태에서 타입 검사를 추가한 것이라고 보면 됩니다.

언어 외적 특징

타입스크립트로 작성된 test.ts 파일은 자바스크립트의 파일처럼 바로 실행될 수 없습니다. 타입스크립트로 작성된 코드를 사용할 때는 그 코드를 자바스크립트 코드로 변환하는 트랜스파일 되는 과정이 추가됩니다. 그래서, 타입스크립트 프로젝트에는 tsconfig.json 파일 같은 타입스크립트 컴파일러 설정 파일 등이 추가적으로 존재하게 됩니다. 그리고 tsc 와 같은 타입스크립트 컴파일러가 이 설정 파일들을 참고해 자바스크립트로 변환하게 됩니다.
컴파일 과정이 있기 때문에, 타입스크립트가 코드를 보고, 타입 검사 시 문제가 있다면 자바스크립트로 트랜스파일이 되지 않아 실행할 코드가 생성되지 않을 수 있습니다.
이러한 특징 때문에 타입스크립트는 그 자체로 디버깅하는 것은 쉽지 않습니다. 그래서 저는 타입을 아주 복잡하게 정의하면 안 되겠다는 생각을 하게 되었습니다.

역할

타입스크립트는 자바스크립트가 해주지 못하는 언어 검사를 자동으로 해줍니다. 그런데, 생각보다 더 자세히 해줍니다. 타입스크립트는 다음과 같은 역할을 해줍니다.
  1. 타입추론: 내가 모든 값이나 객체에 타입 선언을 하지 않아도, 이 타입이 무엇일지 자동으로 추론해줍니다.
  1. 타입정의: 내가 원하는 타입을 선언할 수 있도록 기능적으로 지원합니다.
  1. 타입구성: 정의한 타입끼리 연산해 새로운 타입을 만들어낼 수 있습니다.
  1. 구조적 타입 시스템: 타입추론의 방식입니다. 타입 검사를 할 때, 값의 형태를 기준으로 검사합니다. 즉, 값의 형태만 같다면 같은 타입이라고 취급합니다.
interface Point { x: number; y: number; } function printPoint(p: Point) { console.log(`${p.x}, ${p.y}`); } // "12, 26"를 출력합니다 const point = { x: 12, y: 26 }; printPoint(point);
point 변수는 Point 타입으로 선언한 적은 없지만, Point 형태를 비교해 같다면, 타입스크립트에서는 통과하게 됩니다.
type A = { a: string }; type B = { b: string }; type C = A & B; const object = { a: 'a'; b: 'b'; c: 'c'; }; function printC(o: C) { console.log(`${o.a} ${o.b}`); }; printC(object);
object는 a 와 b 를 속성으로 가지고 있으므로 타입스크립트에서는 통과하게 됩니다.

이해

저는 타입스크립트의 타입은 집합으로, 실제 객체는 집합에 속할 수 있는 원소로 이해하고 있습니다. 타입스크립트의 타입정의하는 것은 집합의 특징을 정의하는 것으로, 타입구성집합끼리의 특정 연산으로 새로운 집합을 만드는 것으로, 구조적 타입 시스템을 통해 타입추론을 하는 것은 어떤 객체를 기준으로 타입을 바라볼 때 원소(객체) 가집합(타입)에 속하면 통과하는 것으로 이해하는 것이죠. 저는 타입챌린지를 하면서 익숙해져야하는 다양한 연산 때문에 집합이라고 이해하는 것이 편했습니다.

타입스크립트에서 지원하는 다양한 기능들

타입정의

이미 정의해둔 타입들

  1. 원시타입: string number boolean bigint null undefined symbol (여기서 bigint 는 21억이 넘는 큰 정수를 다룰 때 사용하는 BigInt 객체와 호환됩니다.)
  1. 배열 형태: string[] number[] T[] 와 같이 배열 형태로 갈 수 있습니다.
  1. any: 타입 검사를 하지 않고 싶을 때 사용하는 타입입니다.
  1. never: 어떤 타입도 never 를 상위 타입으로 가질 수 없습니다. 집합으로 치면 공집합이라고 생각할 수 있습니다.
  1. unknown: 개인적으로 가장 헷갈리는 타입이었습니다. unkown 은 모든 타입의 상위 타입입니다. 집합으로 치면 전체집합이라고 생각할 수 있습니다. 하지만, unkown 이 전체집합이라고 해서 모든 객체가 다 포함 될 수 있다고 생각하면 안 됩니다. 오히려 반대로 전체집합이기 때문에, 특정 조건이 있다고 할 수 없습니다. 왜냐하면 무언가 특정 조건이 있다고 단정해버리면, 그렇지 않은 객체는 포함할 수 없게 되기 때문입니다. 그래서 입력으로 모든 타입의 객체를 받는 것을 의도한다면 unknown 을 사용합니다. 그리고 추후에 직접 타입을 구체화 하게 됩니다.

any 타입 사용과 유사한 에러 허용 주석

종종 타입스크립트로 코드를 작성하면 타입에 대한 제약 때문에 컴파일이 되지 않거나 하는 일이 있습니다. 특히, 외부 라이브러리 등 내(우리)가 작성한 코드가 아닐 때는 타입을 파악하기 쉽지 않아 any 를 사용해 넘어가는 경우가 있습니다. 하지만, 이는 추후 내 코드의 타입 파악을 어렵게 할 수 있기 때문에, any 를 써서 넘어가기 보다는 타입스크립트 디렉티브 주석을 사용하는 것도 방법입니다. 또한, 특정 lint 에서는 any 를 사용하면, 에러(빨간줄)을 내기도 합니다.
// @ts-expect-error: third-party lib type mismatch const something: number = "hello"; // 타입 오류를 일부러 허용
디렉티브 주석 사용 예시
디렉티브 주석은 위처럼 에상하는 error 라는 것을 코드 위에 주석으로 추가한 뒤, 사유를 적어주어 관리를 용이하게 합니다. 이 주석을 사용하면, 타입스크립트는 타입이 맞지 않아도 컴파일 에러를 내지 않습니다. 또한, 이 주석을 사용했는데, 타입스크립트 에러가 나지 않는다면, 이 주석에 에러(빨간줄)가 생깁니다.

인터페이스

객체지향언어의 인터페이스 vs 타입스크립트의 인터페이스

우선 타입스크립트의 인터페이스는 자바나 C++ 같은 객체 지향 언어에서의 인터페이스와는 완전히 다른 것으로 생각해야합니다. 객체 지향 언어에서의 인터페이스는 클래스의 추상화된 설계를 작성할 때 사용하는 것인 반면, 타입스크립트에서의 인터페이스는 타입스크립트에서 다루는 타입과 거의 유사합니다. 타입스크립트에서는 인터페이스가 연산의 대상이 될 수 있습니다.
interface A { a: string; } interface B { b: string; } type AandB = A & B; type IterateProps<T> = {[K in keyof T]: T[K]} /** type Test = { a: string; b: string; } */ type Test = IterateProps<AandB>
인터페이스가 연산의 대상이 되는 예시

타입스크립트 타입 vs 타입스크립트 인터페이스

타입스크립트 내부에서도 타입과 인터페이스는 유사하지만, 차이가 있습니다.
인터페이스는 모듈 내 재정의가 가능하고, 자동으로 재정의된 타입이 교집합 연산 한 것 처럼 됩니다. 타입은 모듈 내 재정의가 안 됩니다.
interface A { a: string; } interface B { b: string; } /** 재정의 가능!! 마치 아래와 같은 인터페이스 처럼 취급하게 됩니다. interface A { a: string; b: number; } */ interface A { b: number; } type AandB = A & B; type IterateProps<T> = {[K in keyof T]: T[K]} /** type Test = { a: string; b: never; } */ type Test = IterateProps<AandB>
인터페이스 A 를 재정의 하는 예시
인터페이스 오직 객체 모양을 선언하는데에만 사용되며, 기존 원시 타입에 별칭을 부여하는 방식으로는 사용 할 수 없습니다.
// Here's two examples of // using types and interfaces // to describe an object interface AnObject1 { value: string } type AnObject2 = { value: string } // Using type we can create custom names // for existing primitives: type SanitizedString = string type EvenNumber = number // This isn't feasible with interfaces interface X extends string {}
원시 타입에 별칭 부여 방식이 안된다는 것을 보여주는 예시

타입 연산

집합의 연산과 유사하게 이해할 수 있습니다. 타입스크립트는 기본적인 연산으로 합집합과 교집합 연산을 지원합니다.

합집합

유명한 연산입니다. 다른 언어에서도 자주 사용하는 표현인 | 를 통해 두 타입을 합집합 연산 할 수 있습니다. 이렇게 새로 만들어진 타입은, 합집합이라는 표현에 맞게 타입스크립트가 타입 추론을 할 때, 객체가 가질 수 있는 타입 범위가 넓어지게 됩니다.
// 합집합의 예시 type Slug = string | number; // slug 로 들어오는 객체는 string 타입이나 number 타입 둘 다 허용하게 됩니다. function ToString(slug: Slug) { if(typeof slug === 'string') { return slug; } else if(typeof slug === 'number') { return `${slug}`; } throw Error('잘못된 타입의 인풋이 들어왔습니다.); };

교집합

역시나 유명한 연산입니다. 다른 언어에서도 자주 사용하는 표현인 & 를 통해 두 타입을 교집합 연산 할 수 있습니다. 이렇게 새로 만들어진 타입은, 교집합이라는 표현에 맞게 타입스크립트가 타입 추론을 할 때, 객체가 가질 수 있는 타입 범위가 좁아지게 됩니다.
type Slug = string | number; // 교집합의 예시 type FilteredSlug = Slug & string; // slug 는 원래 string 이나 number 타입 둘 다 가능했지만, 이젠 string 만 가능합니다. function splitSlug(slug: FilteredSlug) { // 그래서 .split 메서드를 안전하게 사용할 수 있습니다. return slug.split('/'); } // 이 함수는 .split 메서드를 호출하려고 하면 타입스크립트에 의해 에러 표시가 나게 됩니다. function nonCertifiedSplitSlug(slug: Slug) { return slug.split('/'); }
처음에 제가 헷갈렸던 부분은 객체의 교집합 연산이었습니다. 교집합 연산을 했는데, 원소가 늘어났다는 이상한 오해를 하게되어 타입챌린지 스터디 초반 한 주를 고생했던 기억이 있습니다. 저는 일단 다른 곳에서 예습으로 타입은 집합이다라는 생각을 했었는데, 타입은 집합의 조건을 정의하는 것이라는 것을 오해해 왜 교집합을 했는데 집합의 원소가 늘어나지? 라는 착각을 하게 되었습니다. 제대로 이해하지 않은 상태에서 무작정 암기식 학습을 해 생겨난 문제였습니다.
interface A { a: string; } interface B { b: string; } type AandB = A & B; type IterateProps<T> = {[K in keyof T]: T[K]} /** 말도안되는 착각이지만, 집합의 원소가 늘었다고 생각했음... 집합의 원소가 늘어난 것이 아니라 집합의 조건이 늘어난 것 이므로, 집합에 해당 될 수 있는 원소들은 줄어들게 됨... type Test = { a: string; b: string; } */ type Test = IterateProps<AandB>
하지만, 객체가 집합에 해당하는 원소고, 타입정의가 집합의 조건을 정의하는 것이라고 이해한다면, 당연히 집합(타입)에 대한 조건(a 와 b 조건이 합쳐짐)이 늘었으므로, 이를 만족하는 원소(객체) 들은 줄어들게 될 것 입니다.
이러한 오해는 저만 그랬던 것이 아니라, 스터디를 진행했던 다른 스터디원 분도 저랑 비슷한 착각을 한 적이 있기에, 정리해 보았습니다.

인덱싱된 접근 타입

type Person = { age: number; name: string; alive: boolean }; type Age = Person["age"]; // number
예시
마치 JS 에서 어떤 객체에 person.age 로 접근하듯 접근하는 방식입니다. 이러한 방식은 챌린지 문제 풀이 시, 생각보다 유용하게 사용됩니다.

조건부 타입 연산

타입스크립트의 타입 정의를 좀 더 자동화해서 정의 할 수 있게 해주는 연산자입니다. 하지만, 다른 객체지향 언어를 경험했다면, 굉장히 어색한 표현이 될 수도 있겠습니다.
type Stringify<T> = T extends number ? `${T}` : T;
extends 는 인터페이스와 사용할 때는 다른 객체지향 언어처럼 마치 상속처럼 속성을 추가하거나 오버라이딩하는 것 처럼 동작합니다. 하지만, 타입 선언 후 extends 문은 마치 조건문을 정의하는 if 문과 비슷한 역할을 하게 됩니다.

조건부 타입 연산의 infer 키워드

조건부 타입 연산에는 infer 라는 키워드를 통해 조건부 타입이 참일 때, 특정 요소에 접근 할 수 있는 키워드가 있습니다.
type FlattenOnce<Type> = Type extends Array<infer Item> ? Item : Type; type Flatten<T extends unknown[]> = T extends [infer First, ...infer Rest] ? First extends unknown[] ? [...Flatten<First>, ...Flatten<Rest>] : [First, ...Flatten<Rest>] : [];
infer 키워드 사용 예시
예시 처럼 정말 다양하게 infer 키워드를 통한 타입 특정 요소 접근이 가능합니다. 제네릭 안에 있는 요소에 접근해 여러가지 속성 재정의를 할 수 있습니다.

분산적 조건부 타입 연산

조건부 타입 연산을 할 때, 제네릭으로 들어온 모든 타입은 유니온 형태를 가정하고 결과에 분배됩니다.
type ToArray<Type> = Type extends any ? Type[] : never; type StrArrOrNumArr = ToArray<string | number>; // string[] | number[] 으로 됩니다. (string | number)[] 가 아니라요.
분산적 조건부 타입 연산 예시
그러면 어떻게 (string | number)[] 형태로 리턴하는 타입을 만들 수 있을까요?
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; // 'StrArrOrNumArr' is no longer a union. type StrArrOrNumArr = ToArrayNonDist<string | number>;
분산적 조건부 타입 연산을 막는 방법 예시
위의 예시와 같이 대괄호로 감싸는 등의 조건부 타입 연산 시 제네릭을 재가공 하면 됩니다. 출처에서는 대괄호로 감싸면 됩니다. 라고만 적혀있지만, 대괄호 뿐만 아니라 어떤 방식으로던 조건부 연산에 들어갈 제네릭을 다른 형태로 재가공하면 분산적으로 동작하지 않게됩니다. 출처에서는 가장 대중적이고 편리한 방법 1개만 설명한 것 입니다.

Mapped type

매핑된 타입이라는 표현으로 해석되는 것으로, 마치 JS 의 forEach() 와 같은 향상된 포문을 도는 느낌으로 사용할 수 있는 표현식입니다.
type IterateProps<T> = {[K in keyof T]: T[K]}
매핑된 타입의 사용 예시
예시와 같이 직접적으로 key 와 value 에 접근 할 수 있습니다.

템플릿 리터럴 타입

타입도 리터럴 값들로도 지정할 수 있는데요. 특히, 템플릿 리터럴 타입도 사용이 가능합니다.
type Concat<A extends string, B extends string> = `${A}, ${B}`;
템플릿 리터럴 타입의 사용 예시
그리고, 템플릿 리터럴 타입 또한 조건부 연산과 infer 를 통한 타입 접근이 가능한데요. 템플릿 리터럴의 조건부 연산의 infer 는 패턴매칭 방식으로 매칭합니다.
type FindX<Str extends string> = Str extends `${string}X${string}` ? true : false; type FindXTest1 = FindX<'X'>; // true type FindXTest2 = FindX<'A'>; // false type FindXTest3 = FindX<'AX'>; // true type FindXTest4 = FindX<'XA'>; // true type FindXTest5 = FindX<'AXA'>; // true
템플릿 리터럴 infer 패턴매칭 사용 예시
위의 타입은 문자열 리터럴을 제네릭으로 받으면, 그 문자열을 검사해 ‘X’ 문자가 있으면 true, 없으면 false 가 되게 해주는 타입입니다.

타입챌린지 이후 재해석한 타입스크립트의 연산들

문제를 풀 때, 저는 타입 정의를 함수로, 타입의 제네릭을 인자로, 그리고 extends 문을 조건문으로 시작하며, 재귀적으로 반복적인 로직을 구조화 하게 되었습니다. 타입스크립트를 통한 타입 정의는 생각보다 더 구체적이고 체계적이게 잘 활용할 수 있습니다. 라이브러리 형태의 코드를 개발하고, 이를 배포하신다면, 그리고 그 라이브러리가 타입스크립트를 지원할 것이라면, 타입챌린지를 한번쯤 꼭 경험해보는 것이 좋지 않을까 싶었습니다.
 

후기의 후기

간단하게 후기를 작성하려다가, 제가 공부했던 것을 정리하고자 하는 생각으로 이것저것 적다보니, 엄청 길어지게 되었습니다. 이 글은 타입스크립트 연산 관련된 모든 것을 다룬 것은 아니며, 제가 타입챌린지를 경험하고 중요하고, 유용하게 사용했던 타입스크립트의 기능들을 정리한 것 입니다. 혹시나, 타입챌린지를 알게되어 한번 해보고 싶다고 생각하셨던 분께 운이 닿아 보이게 된다면, 조금이나마 도움이 되었으면 좋겠습니다. 감사합니다.