Typescript의 기본 유틸 타입
타입스크립트를 이용해서 개발을 한지 어느덧 1년이 조금 넘은 것 같다. 현재의 직장으로 옮기기 전에는 VueJS와 타입스크립트 조합으로 사용을 하였고, 지금은 ReactJS와 타입스크립틀 조합으로 사용하고 있다.
타입스크립트로 프로젝틀를 진행을 하며 처음에는 인터페이스와 모듈에 대한 인풋/아웃풋의 타입에만 신경을 쓰고, 비효율적인 인터페이스 선언에 대한 고려는 크게 하지 않았다. 사실은 알면서도 모른 척 했을 수도 있다. 하지만 비지니스가 점점 더 복잡해짐에 따라 인터페이스를 효율적으로 관리해야하는 니즈가 생겼다. 간단한 프로젝트에서는 모델에 대한 인터페이스 선언을 해도 그 수가 많지 않았지만, 지금은 선언된 모듈 인터페이스만 해도 그 수가 기하급수적으로 많아지기 시작했다. 그러던 중 타입스크립트 내에서 제공해주는 유틸 타입을 보게 되었고, 추가적인 라이브러리 내에서 지원하는 유틸 타입을 보게 되었다. 물론 라이브러리에서 지원하는 모든 유틸 타입이 다 필요한 것은 아니기에, 살펴보고 필요하다라고 판단되는 것은 일부 정의해서 쓰기로 했다. 그 중 유용하다고 생각하는 유틸 타입이나 타입스크립트에서 제공해주는 유틸 타입에 대해서 살펴보았다.
타입스크립트에서 기본적으로 제공해주는 유틸 타입
Partial유틸 타입
Partial 타입은 제네릭 타입 T에 대해서 모든 프로퍼티들을 Optional하게 변경한다. 정의된 Patial 타입을 살펴보면 아래와 같이 선언되어져 있다.
1 | type Partial<T> = { |
Patial 타입은 제네릭 타입의 T 타입의 Public Property 들에 대하여 기존의 타입은 유지하되, 각각의 Property 들을 Optional 타입으로 변경해준다.
keyof T 와 T[K]
Indexed type query인 keyof T는 제네릭 타입 T의 Public Property 들에 대한 Union 타입이다. 우리에게 익숙한 TODO에 대한 인터페이스를 통해 살펴보면 다음과 같다.
1 | interface Todo { |
이러한 keyof 는 문자열(String) 타입의 서브 타입이다. 이러한 keyof 키워드와 함께 자주 쓰이는 연산자가 있다. 위에서 Partial 타입에서 살펴봤던 T[K]이다. Indexed access operator 인 T[K] 연산자 덕분에 Partial 타입이 기존의 타입을 유지한 상태에서 각각의 Property들을 Optional 한 타입으로 변경해줄 수 있었다. Indexed type query와 Index access operator를 응용한다면 아래와 같이 type-safe 한 값들을 가져올 수 있다.
1 | function getValue<T, K extends keyof T>(todo: T, key: K): T[K] { |
1 | interface Todo { |
이러한 Partial 타입을 이용하여 인터페이스 안에 혼재 되어 있는 타입들에 대하여 Required 타입과 Optional 타입을 분리할 수 있다는 장점이 있다. 사용자 정보에 대한 모델 타입이 있다고 가정해보자.
1 | interface UserInformation { |
물론 이러한 형태로 작성해도 문제는 없을 것이다. 하지만 상황에 따라 Required 타입와 Optional 타입을 분리해야할 상황이 있다. 예를 들어 별도의 API 거쳐 Service Layer 를 통해 Optional한 값에 대하여 기본값을 추가해주는 일종의 Generate 함수의 Parameter 등이 있을 것이다. (이에 대한 자세한 설명은 비즈니스 코드를 작성하는 개인의 취향 혹은 팀 내의 컨벤션이므로 깊이있게 설명하진 않는다.) 이러한 경우 아래와 필요성에 따라 아래와 같이 분리할 수 있다.
1 | type UserInformation = RequiredUserInformation & Partial<OptionalUserInformation>; |
Required타입
Required 타입은 앞서 살핀 Partial 유틸 타입과는 반대의 개념이다. Partial 유틸 타입은 모든 프로퍼티를 Optional로 만들어줬다면 Required 타입은 제네릭 타입 T의 모든 프로퍼티에 대해 Required 속성으로 만들어준다.
1 | type Required<T> = { |
Patial 타입과 동일하게 기존의 값은 유지된 상태에서 Requied 타입으로 변경된다는 것을 꼭 인지하도록 하자.
1 | interface OptionalTodo { |
Readonly
Readonly 타입을 이용하면 주어진 제네릭 타입 T의 모든 프로퍼티가 readonly 속성을 갖도록 변경한다.
1 | type Readonly<T> = { |
readonly를 이용하는 대표적인 경우가 React 컴포넌트의 Props와 State 값이 이에 해당한다. 우리는 모두 알듯, React 내에서의 모든 Props 와 State 는 Immutable이다. 그렇기 때문에 직접적으로 변경해서는 안된다. 이러한 상황에서 Readonly 타입을 이용할 수 있다.
1 | class Component<P, S> { |
혹은 immutable-js과 같은 데이터의 불변성을 보장해주는 라이브러리나 혹은 Javascript의 freeze 함수에 사용할 때 유용하게 사용할 수 있다. 예를 들어 Object를 얕은 동결을 지원하는 freeze 함수를 사용하는 경우 모든 함수의 경우 readonly 속성을 가져야 함으로 이럴 때 유용하게 사용할 수 있다.
1 | function freeze<T>(obj: T): Readonly<T> { |
Pick<T, K>
Pick 타입은 주어진 첫번째 제네릭 타입 T 내에서 Union 타입 K에 대한 프로퍼티에 대한 타입들을 뽑아낸다.
1 | type Pick<T, K extends keyof T> = { |
익숙한 TODO를 통해 Pick 타입을 사용하는 예제를 살펴보도록 하자.
1 | interface Todo { |
이러한 Pick 타입을 이용한다면 필요한 타입만 추출하여 원하는 새로운 타입을 만들 수 있다.
Record
Record 타입은 총 두개의 제네릭 타입을 받을 수 있다. 첫번째 제네릭 타입 K은 프로퍼티 타입으로, 두번째 제네릭 타입 T은 값의 타입으로 사용된다.
1 | type Record<K extends keyof any, T> = { |
Record 타입의 구조체를 살펴보면 일반적으로 사용하는 Object와 닮은 꼴을 하고 있다는 것을 알 수 있다. 아래와 같이 사용할 수 있다.
1 | type IFooBar = { |
Exclude
Exclude 타입은 2개의 제네릭 타입을 받을 수 있으며, 조건부 타입(Conditional type)을 이용하여 타입을 정의 한다.
1 | type Exclude<T, U> = T extends U ? never : T; |
두번째 제네릭 타입에 대하여 첫번째 제네릭 타입이 할당 가능한 타입(Assignable)인지를 여부를 판단하여 할당 가능한 타입을 제외한 나머지 타입들을 이용하여 타입을 정의한다. 이해하기 쉽도록 아래의 예제를 살펴보도록 하자.
1 | interface Todo { |
위에서 살펴본 할당 가능한(Assignable) 타입을 제외한다면 Cotents 타입은 어떠한 타입을 가지게 될까 ? 두번째 제네릭 타입 U에 해당하는 Memo에 대하여 첫번째 제네릭 타입에 해당하는 Todo와 Memo 중 할당 불가능한 타입은 Todo 타입이 될 것이다.
Extract
Extract 유틸 타입은 단어 그대로 추출의 의미를 가지며, Exclude 타입과 반대의 타입이다. 첫번째 제네릭 타입 U에 대하여 제네릭 타입 T 중 할당 가능한 타입을 할당한다.
1 | type Extract<T, U> = T extends U ? T : never; |
Exclude 타입에서 살펴보았던 예제를 다시 한번 살펴보도록 하자.
1 | type Todo = { |
Exclude 타입과는 다르게 유니온 타입 Todo과 Memo에 대하여 Memo에 할당 타입을 반환하기 때문에 Contents 타입은 Memo 타입이 되는 것을 알 수 있다.
Omit
Omit 타입은 두개의 제네릭 타입을 받으며 첫번째 제네릭 타입 T에 대하여 두번째 제네릭 타입 K의 값을 제외한 나머지 값을 반환합니다.
1 | type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
정의된 Omit의 타입을 살펴보면 앞서 우리가 익혔던 타입들을 이용하여 정의하고 있다. 예제를 통해 정의된 타입들을 Todo 인터페이스의 예제를 가지고 이해해보도록 하자.
1 | interface Todo { |
일단 Exclude 타입을 통해 살펴보면 keyof T 에 해당하는 Todo 타입의 프로퍼티 중 두번째 제네릭 타입 K에 해당하는 id를 제외한 나머지 값들을 뽑아낸다.
1 | Pick<Todo, "title" | "isDone">; |
그렇게 Exclude를 통해 뽑아낸 유니온 타입들에 대하여 Pick 타입을 이용하여 Todo 타입 중 그에 해당하는 프로퍼티의 타입을 뽑아낸다.
1 | interface Todo { |
NonNullable
NonNullable 유틸타입은 주어진 제네릭 타입 안에서 null이나 undefined을 제거한다.
1 | type NonNullable<T> = T extends null | undefined ? never : T; |
이미 앞에서 살펴봤던 타입들을 모두 이해했다면 이와 같은 Conditional type(조건부 타입)은 이해하기 어렵지 않을 것이다. 간단한 예제를 통해 살펴보면 후, 넘어가도록 하자.
1 | type Todo = { |
ReturnType
ReturnType 타입은 주어진 제네릭 타입 T의 return type을 할당한다.
1 | type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; |
여기에서 익숙하지 않은 infer 키워드가 등장하는데, 간단히 이야기하자면 해당 타입을 추론
하고자 할 때 사용하는 키워드라고 생각하면 된다. 관련해서 조금더 자세히 알아보고자 한다면 https://dev.to/aexol/typescript-tutorial-infer-keyword-2cn 를 참고하도록 하자. 그래서 위에서의 반환 타입을 분석해보자면, T extends (...args: any) => infer R ? R : any
는 R 타입에 대해서 타입 추론이 가능하다면 R 타입을 그렇지 않다면 any 타입을 반환한다. ReturnType 의 경우 해당 함수에 대한 반환 타입을 타이핑할 때 사용할 수 있다. 아마 Redux 를 사용해본 사람이라면 이러한 ReturnType을 ActionCreator 함수에서 활용할 수 있음을 깨달을 수 있을 것이다.
1 | interface IPayload { |
기타 유용한 유틸 타입
타입스크립트에서 제공해주는 유틸 타입을 제외하더라도 개인적으로 커스텀해서 사용하기 좋아하는 타입들 역시 있다. 이러한 유용한 유틸 타입 중 일부에 대해서는 아래의 링크에 conditional-type-checks
를 참고하였으며, 기타는 회사에서 업무를 진행하며 필요하다는 생각이 들어 추가한 것들도 있다.
Nullable
Javascript로 개발을 할때는 Object를 초기화할때 null을 이용하여 초기화 시켜줬다. 이러한 객체는 선언시 null 타입을 가질 수도 혹은 이후 할당된 객체의 값을 가질 수 있어야 한다.
1 | type INullable<T> = T | null; |
개인적으로는 보통 서버와의 통신을 제어하는 래퍼 함수(Wrapper function)의 리턴 타입의 인터페이스에 사용한다. 이렇게 작성된 인터페이스로 인해 일차적으로는 서버의 응답값과의 정합률이 높아질 뿐만 아니라 자칫 놓치고 넘어갈 수 있는 방어 코드에 대해서도 컴파일 단계에서 미리 알아차릴 수 있다는 장점이 있다.
Maybe
프론트 개발을 하다보면 아무리 정해져 있는 API 규약에 맞춰 백엔드 서버와 커뮤니케이션 한다고 하더라도 실제 정해져있는 규약으로 넘어오지 않는 경우가 많다. 그러한 경우 값이 없을 때의 타입이 null인지, undefined인지 알 수가 없다. 물론 아예 해당하는 키의 데이터가 안내려올때도 있지만 그러한 경우는 optional 옵션을 이용하여 인터페이스를 선언하면 되지만, 타입이 명확하지 않은 경우는 런타임 환경에서 타입 에러에 직면하는 경우가 많다. 그런 경우를 대비하며 Nullable 타입과 분리하여 Maybe 유틸 타입을 선언하여 사용한다.
1 | type IMaybe<T> = T | undefined | null; |
이 외에도 AJAX의 응답값에 대한 래퍼 함수의 리턴 타입 등을 커스텀해서 사용하고 있지만, 이외에는 생각나는 유틸 타입은 없는 것 같다. 이 외에도 이러한 유용한 유틸 타입을 제공해주는 라이브러리가 있다. 혹시나 또다른 유틸 타입이 필요하다면 아래의 참고 링크를 참고하는 것도 좋을 것 같다.
혹시나 이 글을 보시고, 커스텀으로 사용하고 있는 또다른 좋은 유틸 타입이 있다면 여러분의 타입도 한번 공유 부탁드립니다.😄