Typescript의 유틸리티 타입 (2) - Partial, Required, ReadOnly, Omit, NonNullable, ReturnType

ggong·2021년 7월 14일
8

타입스크립트를 야금야금 조금씩 알아봅시당 2222

지난번에 정리한 글에서 이어져요
Typescript의 유틸리티 타입 (1) - Record, Exclude, Extract, Pick

5) Partial<T>

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

type Partial<T> = {    
  [P in keyof T]?: T[P];
};

제네릭 타입의 프로퍼티들에 대해 기존의 타입은 유지하되, 각각의 프로퍼티들을 Optional 타입으로 변경해준다. 필수 타입과 Optional 타입을 구분해서 사용해야 하는 경우 아래와 같이 쓸 수 있다.

type UserInformation = RequiredUserInformation & Partial<OptionalUserInformation>;

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

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

6) Required<T>

Required는 위의 Partial과 반대되는 개념이다. 제네릭 타입 T의 모든 프로퍼티에 대해 Required 속성으로 만들어준다.

type Required<T> = {
  [P in keyof T]-?: T[P];
};

마이너스 연산자는 Optional을 제거해준다는 의미의 연산자이다.
Partial 타입과 동일하게 기존의 타입은 유지된 상태에서 Required 타입으로 변경된다.

interface OptionalTodo {
  id: string;
  text?: string;
  isDone?: boolean;
}

type Todo1 = Required<OptionalTodo>;

interface Todo2 {
  id: string;
  text: string;
  isDone: boolean;
} // 타입 Todo1과 타입 Todo2 는 동일한 타입이다.

7) ReadOnly<T>

T의 모든 프로퍼티를 읽기 전용(readOnly)으로 설정한 타입을 구성한다. 즉 모든 프로퍼티의 값을 변경할 수 없고 참조만 할 수 있도록 만든다.

type ReadOnly<T> = {
  readonly [P in keyof T]: T[P];
};

ReadOnly로 생성된 타입에 값을 재할당하려고 하면 에러가 난다.

interface eventState {
  title: string;
  id: number;
}

const event: ReadOnly<eventState> = {
  title: 'new event',
  id: 123,
};

event.title = 'Hello'; // 에러 발생!

자바스크립트에서 Object.freeze() 메소드를 통해 객체를 동결하여 사용하는 경우, 이렇게 frozen된 객체에는 새로운 속성을 추가하거나 이미 있는 속성을 삭제 혹은 변경할 수 없다. frozen 객체의 프로퍼티에 재할당 하려고 하는 경우 strict mode에서는 에러가 난다.
ReadOnly 유틸리티 타입을 사용하면 마찬가지로 재할당시에 에러를 뱉는다.


interface subProps {
  eventId: string;
}

const freeze = <T extends subProps>(obj: T): Readonly<T> => {
  return obj;
};

const originalData = {
  eventId: 'event',
};

const newData = freeze(originalData);
newData.eventId = 'hello'; // Cannot assign to 'eventId' because it is a read-only property.

8) Omit<T, K>

Omit 타입은 두개의 제네릭 타입을 받으며 T에서 모든 프로퍼티를 선택한 다음 K를 제거한 타입을 구성한다.

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

위의 설명이 바로 이해되지 않았는데, 아래처럼 이해하면 좀더 쉽다.

type Event = {
  id: number;
  title: string;
  detail: string;
};

// Event에 속하는 타입들 중 detail를 제외한 타입들을 BasicEventInfo에 넣는다.
type BasicEventInfo = Exclude<keyof Event, "detail">;

// Event에서 BasicEventInfo에 있는 키값에 속하는 프로퍼티들을 리턴한다.
type BasicEventType = Pick<Event, BasicEventInfo>;

//결과 type BasicEventType = { id: number; title: string; };

Pick과는 반대로 T 타입으로부터 K 프로퍼티를 제거한 타입을 구성한다고 볼 수 있다.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
};

보다보니까 Exclude와 다른점이 뭘까라는 생각이 들었는데, Exclude는 두번째 제네릭 타입에 타입이 들어가고 Omit은 키값으로 넣어준다는 게 차이점인 것 같다.
Exclude<T, U> : T에서 U에 할당할 수 있는 타입을 제외한 타입 반환
Omit<T, U> : T의 모든 프로퍼티 중 U가 제거된 프로퍼티들의 타입 반환

9) NonNullable<T>

주어진 제네릭 타입 T에서 null과 undefined를 제외한 타입을 구성한다.

type NonNullable<T> = T extends null | undefined ? never : T;

null 혹은 undefined 타입이거나 상속한다면 무시하고, 아니라면 타입을 리턴한다.

type NotNullType = NonNullable<string | number | undefined>;
// NotNullType은 string | number

10) ReturnType<T>

유틸리티 타입 중에 제일 자주 봤던 타입이다.
ReturnType 타입은 주어진 제네릭 타입 T의 return type을 할당한다.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

음 어려워
T는 (…args: any) => any를 상속한다. 모든 타입의 파라미터를 인자로 받고 결과값으로 모든 타입의 값을 리턴하기 때문에 사실상 모든 함수를 상속한다.
infer라는 키워드는 타입을 추론할 때 사용하는데, 추론된 타입 값을 R에 할당해준다.
T extends (...args: any) => infer R ? R : any는 R 타입에 대해서 타입 추론이 가능하다면 R 타입을, 그렇지 않다면 any 타입을 반환한다. 쉽게 말해서 함수의 리턴 타입을 반환한다.
예시를 좀더 보자.

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void

interface Payload {
  foo: string;
  bar: string;
}

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

type FooBarCreator = ReturnType<typeof fooBarCreator>;
// type IFooBarCreator = Payload

RTK를 사용할 때도 아래와 같은 코드를 쓴다. useSelector를 사용할 경우 사용하는 곳에서 매번 state의 타입 정의를 해주는 대신 useAppSelector라는 hook을 만들어서 사용하는데, 여기에 주입해주는 state 타입을 가져올 때 아래와 같은 코드를 사용한다.

import { combineReducers } from '@reduxjs/toolkit';

const rootReducer = combineReducers({});
export type RootState = ReturnType<typeof rootReducer>;

rootReducer 함수가 리턴한 결과의 타입을 가져와서 RootState에 할당한다.



참고 :
Typescript의 기본 유틸 타입
(https://blog.martinwork.co.kr/typescript/2019/05/28/typescript-util-types.html)
타입스크립트 핸드북
(https://www.typescriptlang.org/ko/docs/handbook/utility-types.html)
Typescript 유틸리티 클래스 파헤치기
(https://medium.com/harrythegreat/typescript-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0-7ae8a786fb20)

profile
파닥파닥 FE 개발자의 기록용 블로그

0개의 댓글