Pandaman Blog

[Typescript] Utility Types - 1 본문

Front end/Typescript

[Typescript] Utility Types - 1

oyg0420 2022. 2. 21. 21:19

타입스크립트에서는 여러 유틸리티 타입을 제공한다. 이 유틸리티 타입은 Global 하게 사용가능하다.

이번 시간에는 유틸리티 타입의 종류와 왜 사용하는지? 어떻게 구현할 수 있는지 확인해 보려고 한다.

1. Partial

제네릭을 통해 넘겨받은 타입의 모든 속성을 optional로 변형해 반환하는 타입이다. 매우 간단하다.

링크

interface Person {
  name: string;
  age: number;
}

function introduce(person: Partial<Person>) {
   return `my name is ${person.name} and ${person.age} years old.`
}

introduce({ name: 'coo' });
introduce({ age: 33 });
introduce({ name: 'coo', age: 33 });

Person 타입의 속성은 옵션널하지 않지만, Partial를 통해 속성들은 옵션널한 타입으로 반환한다.
Partial은 어떻게 생겼을까? 한번 확인해보자.

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

[1, 2, 3].map((P) => ...)

타입을 다양하게 사용하기 위해 제네릭<>을 사용한다. 타입 매개변수을 T 로 정의한다. 첫번째로 [P in keyof T] 구문에 대해서 알아보자.
keyof operation 은 object 타입의 key 를 반환한다.

예제를 살펴보자.
링크

type Person = { name: string; age: number; 2022: 2022 };
type P = keyof Person;
// 'name' | 'age' | 2022
function getPerson(property: P) {
    console.log(property);
}
getPerson('name');
getPerson('age');
getPerson(2022);

keyof 연산자를 통해서 해당 속성을 유니온 타입으로 반환한다. 즉 keyof T의 의미는 타입 매개변수 T 속성의 속성들을 조합을 반환한다. 값이 아닌 속성을 타입으로 반환한다는걸 기억하자

돌아와서 in 키워드에 대해서도 알아보자.
링크

type Person = { name: string; age: number; };
type P = keyof Person; // 'name' | 'age'
type ResultPersonType = { [K in P]: Person[K] };
// { name: string; age: number }

in 키워드로 유니온 타입을 순회 한다고 한다. P 에 해당하는 nameage가 순회한다.
K가 'name'일때 Person['name']string 이 되어 { name: string }이 되고,
다음 K가 'age'일때 Person['age']number 이 되어 { age: nunmber }가 된다.

{ name: string; age: number }

이제 [P in keyof T] 뒤에 있는 ?만 알면 된다. 이미 우리는 알고 있다. 속성 뒤에 ?는 옵션널 타입을 만들어 낸다는 것을..

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

정리하자면, 제네릭으로 들어온 타입 매개변수 T(<T>)의 속성을 유니온 타입으로 반환(keyof T)하고 각 속성을 순회하여 (in) [P]?: T[P]를 만들어 낸다.

2. Required

requried 타입은 모든 속성을 필수 타입으로 지정한다.

링크

interface OptionalType {
  a?: number;
  b?: number;
}

const optional: OptionalType = { b: 123 };

const required: Required<OptionalType> = { b: 123 };
// Property 'a' is missing in type '{ b: number; }' but required in type 'Required<OptionalType>'.

그렇다 옵션널인(?) 타입을 모두 필수로 만들어준다. 예제를 살펴보면 Required<OptionalType>로 인해 b 속성만 할당했을 경우 타입에러가 발생한다.
Required타입 내부는 어떻게 생겼을까.

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

이제 우리는 제네릭(<T>)을 알고, keyof, in에 대해서 알고 있다. 하지만 아직 모르는게 있다 바로 -? 이다. -는 제거한다는 의미이다. 그리고 -?는 ? 식별자를 제거한다는 의미이다. 따라서 모든 옵션널 타입(?)은 제거되어 필수 타입이 되는것 이다. 사실 이키워드는 Mapped Type 에서만 사용할 수 있다.

3. ReadOnly

단어만 봐도 알 수 있다. 수정은 안되는 타입으로 반환한다.

링크

interface Person {
  name: string;
  age: number
}

let person: Readonly<Person> = {
  name: 'coo',
  age: 20
};

person.name = 'I want to change my name';
// Cannot assign to 'name' because it is a read-only property.

변수 person에 할당한 값은 더이상 변경 할 수 없다. Readonly 타입은 아래와 같이 생겼다.

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

readonly 가 [P in keyof T] 앞에 붙는다. 여기서 위에서 배운 -를 응용해보자. 어떻게 될까.

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

let changablePerson: RemoveReadonly<Readonly<Person>> = {
  name: 'coo',
  age: 33
};

changablePerson.age = 20;

오.. Readonly<Person>로 변경된 타입을 RemoveReadonly의 타입매개변수로 전달했다. RemoveReadonly-readonlyreadonly를 제거한다. 응용하면 점차 타입을 갖고 놀 수 있다.

4. Record

Record 타입은 두개의 타입 매개변수를 받는다. 첫번째 타입 매개변수는 속성이 되며, 두번째 매개변수는 속성 값의 타입이 된다.
한번 확인해보자.
링크

interface Person {
 경력: number;
 취미: string;
}

type FrontChapter = "bk" | "lio" | "blue" | "dali" | "coo";

const frontChapter: Record<FrontChapter, Person> = {
  bk: { 경력: 10, 취미: "게임" },
  lio: { 경력: 9, 취미: "독서" },
  blue: {경력: 8, 취미: "축구"},
  dali: { 경력: 7, 취미: "헬스" },
  coo: { 경력: 6, 취미: "청소" },
};

FrontChapter 에 정의된 유니온 타입은 Record의 첫번째 타입인자에 할당되면 각각의 속성이 된다. Person 타입은 두번째 타입인자에 할당되며 각 속성의 값 타입으로 반환된다.

내부가 또 궁금하다.

type RecordTest<K extends keyof any, T> = {
    [P in K]: T;
};

// K = 'bk' | 'lio' | ...
// T = { 경력: ..., 취미: ...}
// K = T
// bk: { 경력: ..., 취미: ...}
// lio: { 경력: ..., 취미: ...}
// ...

keyof any가 보인다. 신경쓰이지만 한번 확인해보자. type AnyType = keyof any;로 정의하고 마우스를 올려보자. type AnyType = string | number | symbol 이게 된다. Kstring | number | symbol을 extends 한 타입이라고 할 수 있다. 따라서 첫번째 타입 매개변수에는 string | number | symbol 의 유니온 타입으로 정의된 타입만 할당할 수 있다. 그리고 순회하여 P: T을 만들어 낸다.
약간 복잡해졌지만 타입매개변수를 2개 받을 수 있다는점을 알게 되었고, extends 를 사용하여 원하는 타입매개변수의 타입을 한정시킬 수 있다는 점도 알 게 되었다.

5. Pick

Pick<Type, Keys>은 단어를 유추해보면 알 수 있다. 타입 매개변수 Type에서 Keys에 해당하는 속성 타입을 Pick 하는것이다.
링크

type Person = {
 name: string;
 age: number;
 job: string;
};

type PersonPreview = Pick<Person, 'name' | 'age'>;

// type PersonPreview = {
//     name: string;
//     age: number;
// }

첫번째 타입 매개변수는 Person 이고, 두번째 매개변수는 'name' | 'age' 로 Person의 속성을 타입으로 갖는다. 이제는 고민해서 Pick 유틸타입을 만들어보자.

첫번째 타입인자를 두개를 받는다. 두번째 인자는 첫번째 타입인자의 속성을 타입으로 갖는다.('name' | 'age')
그럼 아래까지 작성할 수 있겠다. 첫번째 타입인자의 속성을 타입으로 갖는다keyof Type로 표현가능하다. 그리고 extends를 추가하여 keyof Type를 확장한 타입이라고 명시하여 keyof Type에 해당하는 타입만 두번째 타입인자로 지정할 수 있다.

type PickTest<Type, Keys extends keyof Type>

그리고 Keys는 순회되어야하며 { name: string; age: number } 형태로 만들어주어야 한다.
nameageKeys 로 즉 Keys = 'name' | 'age'로 이루어져 있다.

type PickTest<Type, Keys extends keyof Type> = {
 [Key in Keys]: any;
}

우리는 각 속성에 해당하는 타입 지정해야한다. 첫번째 타입 매개변수 Type을 생각해보자.
KeyType에 접근하게 되면 해당 Key의 값의 타입을 도출할 수 있다.
ex) Key = 'age', Type = Person, Type[Key] = Person['age']

type PickTest<Type, Keys extends keyof Type> = {
 [Key in Keys]: Type[Key];
}

'Front end > Typescript' 카테고리의 다른 글

[Typescript] Utility Types - 2  (0) 2022.03.27
[Typescript] Conditional Types  (0) 2022.03.13
[Typescript] Mapped Types  (0) 2022.02.24
[Typescript] 함수  (0) 2021.12.19
[Typescript] Type Guard  (0) 2021.12.18
Comments