Frontend/Today I Learned

[TS] TypeScript 시작하기 - 2

joycie416 2024. 9. 25. 22:24

오늘은 어제 예고한대로 Generic과 Utility type에 대해 정리해보려고 한다.

 

1. Generic 제네릭

제네릭은 지난번 게시글에서 잠깐 보고 지나쳤다.

let oneToSeven: number[] = [1, 2, 3, 4, 5, 6, 7];
let oneToSeven: Array<number> = [1, 2, 3, 4, 5, 6, 7];

let array: (string | number)[] = ["Apple", 1, 2, "Banana", "Mango", 3];
let array: Array<string | number> = ["Apple", 1, 2, "Banana", "Mango", 3];

 

이처럼 `<>`내부에 작성하는 방법을 generic이라고 한다. 이렇게 작성하면 마치 클래스나 함수에 parameter를 입력하는 것처럼 타입을 사용할 수 있다.

function printAnything<T>(arr: T[]): void {
  for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
  }
}

printAnything(['a', 'b', 'c']); // <string>을 써주지 않아도 타입 추론이 가능
printAnything([1, 2, 3]); // <number>를 써주지 않아도 타입 추론이 가능
printAnything<string>(['a','b','c']) // string으로 명시

 

React hook에도 generic을 사용할 수 있다.

import { useState } from "react";

function App() {
  const [counter, setCounter] = useState<number>(1);
  const increment = () => {
    setCounter((prev) => prev++);
  };
  return <div onClick={increment}>{counter}</div>;
}

export default App;

 

Generic도 타입 추론을 하기 때문에 JS에서 하던 것처럼 `useState(1)`이라고 해도 된다.

 

2. Utility type 유틸리티 타입

유틸리티 타입은 generic을 사용해서 타입을 효율적으로 쓸 수 있게 해준다.

 

`Pick<T,K>`

`T`에서 프로퍼티 `K`의 집합을 선택해 타입을 구성함. 이때 `K`는 `T`의 부분 집합이어야 한다.

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

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

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

 

`Omit<T,K>`

`T`에서 `K`를 제외한 모든 프로퍼티로 이루어진 타입을 구성함. `K`에는 `T`에 없는 프로퍼티가 있어도 된다.

위 예시에서 `Todo`에서 `description`을 제외하면 `TodoPreview`타입과 동일함.

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

type TodoPreview2 = Omit<Todo, 'description'>;

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

 

`Excluce<T,U>`

`T`에서 `U`에 할당할 수 있는 모든 속성을 제외한 타입을 구성함. 이때 `U`에는 `T`에 없는 프로퍼티가 있어도 된다.

type T0 = Exclude<"a" | "b" | "c", "a" | "d">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

 

`Extract<T,U>`

`Exclude`와 반대로 `T`에서 `U`에 할당할 수 있는 프로퍼티를 모두 추출해 타입을 구성함.

type T0 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T1 = Extract<string | number | (() => void), Function>;  // () => void

 

`Partial<T>`

`T`의 모든 프로퍼티를 갖지 않아도 되는 타입을 구성함.

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

function update(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: 'organize desk',
  description: 'clear clutter',
};

const todo2 = update(todo1, {
  description: 'throw out trash',
});

console.log(todo1) //{ title: 'organize desk', description: 'clear clutter' }
console.log(todo2) //{ title: 'organize desk', description: 'throw out trash' }

 

`Readonly<T>`

`T`의 모든 프로퍼티를 읽기 전용(read only)으로 설정한 타입을 구성함. 해당 프로퍼티는 재할당이 불가능해진다.

interface Todo {
    title: string;
}

const todo: Readonly<Todo> = {
    title: 'Delete inactive users',
};

todo.title = 'Hello'; // 오류: 읽기 전용 프로퍼티에 재할당할 수 없음

 

` Parameters<T>`와 ` ReturnType<T>`

각각 함수의 parameter와 리턴값의 타입을 정해준다. `Parameters<T>`는 `T`의 파라미터를 배열로 반환해주고, `ReturnType<T>`는 parameter 괄호 옆에 작성해주면 된다.

function log(message: string, userId: number): void {
  console.log(`${userId}: ${message}`);
}

type LogParams = Parameters<typeof log>; // [string, number]

const params: LogParams = ['Hello, world!', 1];
log(...params); // 1: Hello, world!

type LogReturnType = ReturnType<typeof log>; // void

 

여기서 `ReturnType`으로 설정된 `void`는 `log` 함수가 반환하는 값이 없기 때문에 주어진 것이다.

 

` Awaited<T>`

`Promise`의 결과 타입을 추론하는 유틸리티 타입이다. 비동기 함수의 반환 타입을 처리하거나, `Promise`로 감싸진 값을 추출할 때 유용하다.

async function fetchData(): Promise<string> {
    return "Hello, world!";
}

// fetchData 함수의 반환 타입 추론
type FetchDataType = Awaited<ReturnType<typeof fetchData>>;

const data: FetchDataType = await fetchData();
console.log(data); // "Hello, world!"

 

이외에도 위 예시에 나온 `Promise<T>`와 `Record<T>` 등이 있으니 다른 유틸리티 타입은 공식 문서를 참고하자.


사용하다보니 헷갈리는 경우가 많아서 사용 예시를 몇 개 가져왔다.

function add({ a = 1, b = 2 }: { a: number; b: number }): number {
  return a + b;
}

 

export type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

export type Paginate<T> = {
  data: T[];
  first: number;
  items: number;
  last: number;
  next: number | null;
  pages: number;
  prev: number | null;
};

// export async function getTodos (): Promise<Todo[]> {
export async function getTodos() {
  const res = await fetch("http://localhost:4000/todos?_page=1&_per_page=25");
  // const data: Todo[] = await res.json();
  const data: Paginate<Todo> = await res.json();

  return data;
}

 

// function TodoList({ todoList }: { todoList: Todo[] }) {
type TodoListProps = {
  todoList: Todo[];
  onDeleteClick: (id: Todo["id"]) => void; // Todo['id'] : Todo의 'id'의 타입을 가져오기
  onToggleClick: (toggleTodo: ToggleTodo) => void;
};
function TodoList({ todoList, onDeleteClick, onToggleClick }: TodoListProps) {
  return (
    <>
      {todoList.map((todo) => (
        <TodoItem
          {...todo}
          onDeleteClick={onDeleteClick}
          onToggleClick={onToggleClick}
          key={todo.id}
        />
      ))}
    </>
  );
}