Typescript의 유틸 타입

타입스크립트를 이용해서 개발을 한지 어느덧 1년이 조금 넘은 것 같다. 현재의 직장으로 옮기기 전에는 VueJS와 타입스크립트 조합으로 사용을 하였고, 지금은 ReactJS와 타입스크립틀 조합으로 사용하고 있다.

/images/typescript/vue-vs-react-business-perspective-v2.jpg

타입스크립트로 프로젝틀를 진행을 하며 처음에는 인터페이스와 모듈에 대한 인풋/아웃풋의 타입에만 신경을 쓰고, 비효율적인 인터페이스 선언에 대한 고려는 크게 하지 않았다. 사실은 알면서도 모른척했을 수도 있다. 하지만 이직을 한후 비지니스가 점점 더 복잡해짐에 따라 인터페이스를 효율적으로 관리해야하는 니즈가 생겼다. 간단한 프로젝트에서는 모델에 대한 인터페이스 선언을 해도 그 수가 많지 않았지만, 지금은 선언된 모듈 인터페이스만 해도 대략적으로 살폈을 때도 그 수가 기하급수적으로 많아지기 시작했다. 그러던 중 타입스크립트 내에서 제공해주는 유틸 타입을 보게 되었고, 추가적인 라이브러리 내에서 지원하는 유틸 타입을 보게 되었다. 물론 라이브러리에서 지원하는 모든 유틸 타입이 다 필요한 것은 아니기에, 살펴보고 필요하다라고 판단되는 것은 일부 정의해서 쓰기로 했다. 그 중 유용하다고 생각하는 유틸 타입이나 타입스크립트에서 제공해주는 유틸 타입에 대해서 살펴보았다.

일단 기본적으로 타입스크립트는 여러가지 유틸 타입을 제공해준다. 타입스크립트 내에서 제공해주는 유틸 타입을 먼저 살펴보도록 하자.

타입스크립트에서 기본적으로 제공해주는 유틸 타입

Partial유틸 타입

Partial 타입의 제네릭 타입에 대해서 모든 프로퍼티들을 Optional하게 변경한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Todo {
id: string;
title: string;
isDone: boolean;
}

type OptionalTodo = Partial<Todo>;
/**
* type OptionalTodo = {
* id?: string;
* title?: string;
* isDone?: boolean;
* }
*/

이러한 Partial 유틸 타입을 이용하면 인터페이스 안에 혼재 되어 있는 타입을 관심사에 따라 정리할 수 있다는 장점이 있다.

1
2
3
4
5
6
7
8
interface UserInformation {
id: string;
uid: string;
name: string;
age?: number;
profile?: string;
phone?: string;
}

만약 이렇게 optional 프로퍼티와 혼재되어 있는 값이 있다면 아래와 같이 Optional 타입과 Required 타입으로 구분할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
type UserInformation = RequiredUserInformation & Partial<OptionalUserInformation>;

interface RequiredUserInformation {
id: string;
uid: string;
name: string;
}

interface OptionalUserInformation {
age: number;
profile: string;
phone: string;
}

Required유틸 타입

Required 유틸 타입은 앞서 살핀 Partial 유틸 타입과는 반대의 개념이다. Partial 유틸 타입은 모든 프로퍼티를 optional로 만들어줬다면 Required 유틸 타입은 모든 프로퍼티에 대해 optional 속성을 제거한다.

1
2
3
4
5
6
7
8
9
10
11
type OptionalTodo = { 
id?: string;
title?: string;
isDone?: boolean;
};

const todo: Required<OptionalTodo> = {
id: "1",
title: "Todo text",
};
// [Type error] Property 'isDone' is missing in type

Readonly

Readonly 유틸 타입을 이용하면 주어진 제네릭 타입에 대해 모든 프로퍼티가 readonly 속성을 갖도록 변경한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Todo {
id: string;
title: string;
isDone: boolean;
}

type ImmutableTodo = Readonly<Todo>;
/**
* type ImmutableTodo = {
* readonly id: string;
* readonly title: string;
* readonly isDone: boolean;
* }
*/

이러한 readonly 속성은 immutable-js과 같은 데이터의 불변성을 보장해주는 라이브러리나 혹은 Javascript의 freeze 함수에 사용할 때 유용하게 사용할 수 있다. 예를 들어 Object를 얕은 동결을 지원하는 freeze 함수를 사용하는 경우 모든 함수의 경우 readonly 속성을 가져야 함으로 이럴 때 유용하게 사용할 수 있다.

1
2
3
function freeze<T>(obj: T): Readonly<T> {
// do something
}

Record<K, T>

Record 유틸 타입의 경우 주어진 두번째 제네릭 타입 T를 첫번째 제네릭 타입 K의 프로퍼티 타입으로 할당한다. 이러한 타입의 경우 특정 타입의 프로퍼티를 다른 타입을 이용하여 맵핑할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type IFooBar = {
foo: string;
bar: string;
};

type IHelloWorld = 'hello' | 'world';

const x: Record<IHelloWorld, IFooBar> = {
hello: {
foo: 'foo',
bar: 'bar'
},
world: {
foo: 'foo',
bar: 'bar'
}
}

Pick<T, K>

Pick 유틸 타입은 주어진 첫번째 제네릭 타입 T 중 두번째 제네릭 타입 K의 프로퍼티와 일치하는 값을 추출한다.

1
2
3
4
5
6
7
8
9
10
type Todo = {
id: string;
title: string;
isDone: boolean;
};

const todo: Pick<Todo, 'id' | 'title'> = {
id: '1',
title: 'Todo의 title'
};

이러한 Pick 유틸 타입은 React 컴포넌트 중 HOC 컴포넌트를 설계할 때 많이 사용된다.

Extract<T, U>

Extract 유틸 타입은 단어 그대로 추출의 의미를 가진다. 주어진 첫번째 제네릭 타입 T 중에서 두번째 제네릭 타입 U에 할당 가능한 타입들을 추출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Todo = {
id: string;
title: string;
isDone: boolean;
};

type Memo = {
id: string;
title: string;
content: string;
};

type Contents = Extract<Todo | Memo, Memo>;
/**
* type Contents = {
* id: string;
* title: string;
* content: string;
* }
*/
// tslint:disable
type extractFunction = Extract<Todo | Memo | (() => void), Function>;
/**
* type extractFunction = () => void;
*/

Exclude<T, U>

Exclude 유틸 타입은 Extract 유틸 타입과는 반대로 주어진 첫번째 제네릭 타입 T 중 U 타입에 할당 가능한 타입을 제외한 타입을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Todo = {
id: string;
title: string;
isDone: boolean;
};

type Memo = {
id: string;
title: string;
content: string;
};

type Contents = Exclude<Todo | Memo, Memo>;
/**
* type Contents = {
* id: string;
* title: string;
* isDone: boolean;
* }
*/
// tslint:disable
type extractFunction = Exclude<Todo | Memo | (() => void), Function>;
/**
* type extractFunction = Todo | Memo;
*/

NonNullable

NonNullable 유틸타입은 주어진 제네릭 타입 안에서 null이나 undefined을 제거한다.

1
2
3
4
5
6
7
8
9
10
11
12
type Todo = {
id: string;
title: string;
isDone: boolean;
}

type NullableTodos = null | undefined | Todo[];

type Todos = NonNullable<NullableTodos>;
/**
* type Todos = Todo[];
*/

ReturnType

ReturnType 유틸 타입은 주어진 제네릭 타입 T의 return type을 할당한다.

1
2
3
4
5
6
7
8
9
10
interface IPayload {
foo: string;
bar: string;
}

const fooBarCreator = () => ({
foo: "foo", bar: "bar"
});

type IFooBarCreator = ReturnType<typeof fooBarCreate>;

InstanceType

InstanceType 유틸 타입은 주어진 제네릭 타입 T의 인스턴스 타입을 반환한다.

1
2
3
4
5
6
7
class UserState {
id: number;
uid: string;
age: number;
}

type IUserState = InstanceType<typeof UserState>;

기타 유용한 유틸 타입

타입스크립트에서 제공해주는 유틸 타입을 제외하더라도 개인적으로 커스텀해서 사용하기 좋아하는 타입들 역시 있다. 이러한 유용한 유틸 타입 중 일부에 대해서는 아래의 링크에 conditional-type-checks를 참고하였으며, 기타는 회사에서 업무를 진행하며 필요하다는 생각이 들어 추가한 것들도 있다.

Nullable

Javascript로 개발을 할때는 Object를 초기화할때 null을 이용하여 초기화 시켜줬다. 이러한 객체는 선언시 null 타입을 가질 수도 혹은 이후 할당된 객체의 값을 가질 수 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type INullable<T> = T | null;
interface ITodos {
id: string;
text: string;
isDone: boolean;
}

function fetchTodo (): Promise<INullable<ITodos>> {
// request api
}

fetchTodo().then(todo => {
// 서버의 응답값이 null일 수 있기 때문에 방어코드를 추가해준다.
if(!todo) {
// do something
}
});

개인적으로는 보통 서버와의 통신을 제어하는 래퍼 함수(Wrapper function)의 리턴 타입의 인터페이스에 사용한다. 이렇게 작성된 인터페이스로 인해 일차적으로는 서버의 응답값과의 정합률이 높아질 뿐만 아니라 자칫 놓치고 넘어갈 수 있는 방어 코드에 대해서도 컴파일 단계에서 미리 알아차릴 수 있다는 장점이 있다.

Maybe

프론트 개발을 하다보면 아무리 정해져 있는 API 규약에 맞춰 백엔드 서버와 커뮤니케이션 한다고 하더라도 실제 정해져있는 규약으로 넘어오지 않는 경우가 많다. 그러한 경우 값이 없을 때의 타입이 null인지, undefined인지 알 수가 없다. 물론 아예 해당하는 키의 데이터가 안내려올때도 있지만 그러한 경우는 optional 옵션을 이용하여 인터페이스를 선언하면 되지만, 타입이 명확하지 않은 경우는 런타임 환경에서 타입 에러에 직면하는 경우가 많다. 그런 경우를 대비하며 Nullable 타입과 분리하여 Maybe 유틸 타입을 선언하여 사용한다.

1
type IMaybe<T> = T | undefined | null;

이 외에도 AJAX의 응답값에 대한 래퍼 함수의 리턴 타입 등을 커스텀해서 사용하고 있지만, 이외에는 생각나는 유틸 타입은 없는 것 같다. 이 외에도 이러한 유용한 유틸 타입을 제공해주는 라이브러리가 있다. 혹시나 또다른 유틸 타입이 필요하다면 아래의 참고 링크를 참고하는 것도 좋을 것 같다.

혹시나 이 글을 보시고, 커스텀으로 사용하고 있는 또다른 좋은 유틸 타입이 있다면 여러분의 타입도 한번 공유 부탁드립니다.😄


참고

현재 이커머스회사에서 frontend 개발자로 업무를 진행하고 있는 Martin 입니다. 글을 읽으시고 궁금한 점은 댓글 혹은 메일(hoons0131@gmail.com)로 연락해주시면 빠른 회신 드리도록 하겠습니다. 이 외에도 네트워킹에 대해서는 언제나 환영입니다.:Martin(https://github.com/martinYounghoonKim
왜 다시 SSR인가 01
멋사 해커톤 참여기