타입을 잘쓰고 싶은 개발자의 타입챌린지 EASY 레벨 풀이

단단·4일 전
0

글또

목록 보기
9/9

안녕하세요. 단단입니다.
제가 올해 1월 중순부터 FE 개발자들과 '타입챌린지' 스터디를 하고 있습니다.

타입챌린지는 유틸리티 타입 등을 다양한 방식으로 구현하면서 타입 시스템 작동 원리를 이해하고, 문제를 해결하는 챌린지입니다.

최근 리액트 코어를 타입스크립트로 구현하면서 제네릭을 유연하게 사용하고 싶다는 생각이 강하게 들었습니다. 전에 멘토님 2명이 타입챌린지를 해봤을 때 타입을 더 잘 쓰는데 도움됐다고 추천하셔서 타입챌린지 스터디원을 모집해 시작했습니다.

팀원들과 매일 한 문제씩 풀다보니 EASY 레벨 13문제를 다 풀었더라고요!
그래서 (체감 상 EASY하지 않은) EASY 레벨 문제 풀이와 함께 배운 점을 정리해보려고 합니다.

타입을 잘쓰면 좋은 이유

'타입스크립트를 왜 사용하는지'는 면접 단골 질문이기도 합니다.

타입스크립트는 변수, 함수, 객체의 타입을 미리 선언하면서 컴파일 시점에 타입 오류를 예측할 수 있고, 런타임 오류를 줄여 코드 안정성을 높입니다.

실제로 개발을 하면서 null 처리를 제대로 안 했을때 렌더링 자체에 문제가 생겼고, 이를 경험하면서 예측가능한 코드를 쓰는 게 중요하다고 체감했습니다.

또, 타입을 잘 쓰면 코드 편집기에서 자동완성 기능 등을 활용할 수 있기 때문에 좀 더 편리하게 개발을 할 수 있습니다. 즉, 개발자의 생산성을 높이는 데도 도움이 됩니다.

그래서 타입을 잘쓰고 싶다고 생각했고, 타입챌린지를 하고 있습니다.

타입챌린지 EASY 레벨 풀이

4. T에서 K 프로퍼티만 선택해 새로운 오브젝트 타입을 만드는 내장 제네릭 Pick<T, K>을 이를 사용하지 않고 구현하세요.

  • 코드
    type MyPick<T, K extends keyof T> = { [key in K]: T[key] }
  • 풀이
    K는 T의 키들의 부분집합으로, 새로운 오브젝트 타입의 key는 K의 각 프로퍼티를 순회하고, T에서 key 프로퍼티의 타입만 가져옵니다. 타입에서 T[key]는 값이 아니라 타입을 가져옵니다.

7. T의 모든 프로퍼티를 읽기 전용(재할당 불가)으로 바꾸는 내장 제네릭 Readonly<T>를 이를 사용하지 않고 구현하세요.

  • 코드
    type MyReadonly<T> = { readonly [Key in keyof T] : T[Key] }
  • 풀이
    keyof T는 T타입의 모든 프로퍼티 키를 유니온 타입으로 가져오고, Key는 모든 프로퍼티를 순회합니다. 각 속성 앞에 readonly 수식어를 붙여 읽기 전용으로 만들고, T[Key]로 T에서 Key 프로퍼티의 타입을 가져옵니다. Key는 T의 모든 프로퍼티를 순회하니 원래 타입을 유지한다는 의미이기도 합니다.

11. 배열(튜플)을 받아, 각 원소의 값을 key/value로 갖는 오브젝트 타입을 반환하는 타입을 구현하세요.

  • 코드
    type TupleToObject<T extends readonly any[]> = { [P in T[number]]: P }
  • 풀이
    T는 배열(튜플)을 받고, T[number]는 T의 모든 요소를 유니온 타입으로 받습니다. P는 T에서 추출한 리터럴 타입이고, 이를 key로 가진다.
    리터럴 타입은 문자열이나 숫자에 정확한 값을 지정해 더 엄격하게 타입을 지정하는 것입니다.
    in을 사용해 T의 모든 요소를 객체의 key로 사용하고, value도 key와 동일한 값을 가지게 합니다.

14. 배열(튜플) T를 받아 첫 원소의 타입을 반환하는 제네릭 First를 구현하세요.

  • 코드
    type First<T extends any[]> = T extends [] ? never : T[0]
  • 풀이
    T extends []는 빈 배열을 확인할 수 있는 조건으로, 빈 배열 일 때 never를 반환하고, 아닐 때 T의 첫 원소 타입을 반환합니다.
    T.length는 런타임(즉, 자바스크립트)에서 사용하는 방식이라 컴파일 타임에 타입을 검사하는 typescript에서는 사용할 수 없다는 것을 배웠습니다. infer은 조건부 타입 extends 절에서만 사용할 수 있고, infer U에서 U가 추론 가능한 타입이면 참, 아니면 거짓을 반환합니다.

18. 배열(튜플)을 받아 길이를 반환하는 제네릭 Length<T>를 구현하세요.

  • 코드
    type Length<T extends readonly any[]> = T['length']
  • 풀이
    T는 배열(튜플)을 받는데, 배열의 프로퍼티로 length에 접근할 수 있습니다. T.length는 런타임에서 사용하는 방식이라 컴파일 타임에 타입을 검사하는 TS에선 사용할 수 없습니다. readonly 배열을 받게 한 이유는 테스트 케이스에 as const가 있어 해당 배열이 리터럴 타입으로 고정되고, 읽기 전용 배열이 되기 때문입니다.
    배열의 프로퍼티로 length에 접근할 수 있습니다.

43. T에서 U에 할당할 수 있는 타입을 제외하는 내장 제네릭 Exclude<T, U>를 이를 사용하지 않고 구현하세요.

  • 코드
    type MyExclude<T, U> = T extends U ? never : T
  • 풀이
    T를 U에 할당할 수 있으면(=T가 U의 서브타입이면) never를 반환하고, 할당할 수 없으면 T를 반환합니다.
    조건부 타입이 가독성을 해치긴 하지만, 타입의 타입을 정의할 때 종종 쓰입니다.

189. Promise와 같은 타입에 감싸인 타입이 있을 때, 안에 감싸인 타입이 무엇인지 어떻게 알 수 있을까요?

예시: Promise<ExampleType>이 있을 때, ExampleType을 어떻게 얻을 수 있을까요?

  • 코드
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
  ? U extends PromiseLike<any>
  ? MyAwaited<U>
  : U
  : never;
  • 풀이
  1. Promise 타입은 Promise 객체를 타입으로 표현한 TS 내장 타입으로, 비동기 작업의 결과를 나타내며 비동기 작업 완료 후 성공 또는 실패 상태를 반환합니다.
  2. PromiseLike는 then 메서드를 가진 모든 객체를 포함하는 Promise보다 넓은 범위의 타입입니다.
  3. T가 유사프로미스객체 타입이라면 내부 타입을 U로 추론합니다. 조건문에 거짓이면 never를 반환하고, 참이면 U가 유사프로미스객체인지 확인합니다. U가 유사 프로미스 객체이면 MyAwaited를 재귀적으로 호출해 내부 타입을 추출하고, 아니라면 최종적으로 U를 반환합니다.

268. 조건 C, 참일 때 반환하는 타입 T, 거짓일 때 반환하는 타입 F를 받는 타입 If를 구현하세요. Ctrue 또는 false이고, TF는 아무 타입입니다.

  • 코드: type If<C extends boolean, T, F> = C extends true ? T : F
  • 풀이: C는 boolean 타입을 받게 하고, 조건문으로 true를 받을 때와 아닐 때를 나누면 됩니다.

533. JavaScript의 Array.concat 함수를 타입 시스템에서 구현하세요. 타입은 두 인수를 받고, 인수를 왼쪽부터 concat한 새로운 배열을 반환해야 합니다.

  • 코드: type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U];
  • 풀이: 튜플이 존재하기 때문에 readonly any[]로 확장한 T를 사용했습니다. 배열을 결합하는 타입은 스프레드 문법을 사용할 수 있습니다.

898. JavaScript의 Array.includes 함수를 타입 시스템에서 구현하세요. 타입은 두 인수를 받고, true 또는 false를 반환해야 합니다.

  • 코드
    type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest] ? U extends T ? Equal<First, U> extends true ? true : Includes<Rest, U> : false
  • 풀이
    T extends [infer First, ...infer Rest]는 T가 최소 하나의 요소를 가진 배열인가를 확인하고 맞다면 첫 번째 요소 타입을 First로, 나머지 요소들의 타입을 Rest로 추론합니다. U가 T타입을 받으면 Equal 유틸리티 타입을 활용해 First가 U와 같은 타입인지 확인하고 같은 타입이면 true, 같은 타입이 아니면 Rest를 같은 방식으로 확인합니다.

3057. Array.push의 제네릭 버전을 구현하세요.

  • 코드: type Push<T extends any[], U> = [...T, U]
  • 풀이: T는 배열 타입을 받고, 스프레드 문법을 사용해 T배열의 마지막 요소로 U를 추가합니다.

3060. Array.unshift의 타입 버전을 구현하세요.

  • 코드: type Unshift<T extends any[], U> = [U, ...T]
  • 풀이: 스프레드 문법을 사용해 배열 0번째 인덱스로 U를 추가하게 구현합니다.
    알게된 점
    처음엔 아래 코드로 작성했습니다
    type Concat<T extends any[], U extends any[]> = [P in keyof T | P in keyof U]
    근데 배열에 keyof를 쓰면 인덱스가 반환되는 것이더라고요! 배열의 키는 항상 숫자로 된 인덱스이기 때문입니다.

3312. 내장 제네릭 Parameters<T>를 이를 사용하지 않고 구현하세요.

  • 코드: type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer S) => any ? S : never
  • 풀이
    우선, Parameters 내장 제네릭 타입은 함수 타입 T의 매개변수 타입들의 튜플 타입을 구성합니다.
    T는 함수 타입을 받으면 매개변수가 S로 추론될 때 S를 반환하고, 아니면 never를 반환합니다.
    ...args로 매개변수 타입을 쓰면 매개변수 개수와 타입을 제한하지 않는 함수를 나타냅니다.

소감

저는 혼자 공부할 때 보다 스터디를 통해 함께 할 때 의지가 더 생깁니다. 그래서 공부하는 패턴과 환경을 만들기 위해 일부러 여러 개의 스터디를 진행합니다.

이번엔 같은 코드를 구현해도 사람마다 다른 질문과 호기심을 가졌다는 점에서 확실히 혼자 공부할 때보다 더 많은 것들을 배웠습니다.

한 예시로, 팀원이 "배열에 readonly를 사용하면 튜플이 되는 게 아닌가"라는 의문을 가졌습니다.

찾아보니 readonly는 컴파일 시점에만 작동하는데, 길이를 고정하지 않고, 요소 타입을 고정하지 않고, 새로운 배열을 할당할 수 있기 때문에 readonly 배열이 자동으로 튜플이 되진 않는다는 것을 알았습니다.

즉, readonly 배열은 튜플과 달리 길이가 고정되지 않으므로 같은 타입의 요소가 몇 개 들어가든 타입 체킹에서 허용됩니다.

역시 함께 하니 공부하는 즐거움이 더 커진 스터디였습니다! 다음 레벨 문제 풀이도 화이팅입니다!

모든 피드백을 환영합니다. 읽어주셔서 감사합니다.

profile
반드시 해내는 프론트엔드 개발자

0개의 댓글

관련 채용 정보