☁️ goormTIL | TypeScript #56

매루·2025년 11월 27일

goormTIL

목록 보기
54/67
post-thumbnail

📅 2025-11-27

➡️ TypeScript 제네릭, 유틸리티 타입에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리


🔎 학습 리마인드

📌 제네릭

  • 함수, 클래스, 인터페이스, 타입 등을 사용할 때 타입을 외부에서 받아 재사용할 수 있도록 하는 문법
  • 선언 시점이 아니라 사용 시점에 타입을 결정
  • 타입을 보존하면서 유연하게 작성 가능

💡 제네릭 사용 이유

  1. any 대신 타입 정보를 유지하기 위해

    any 타입

    function logText(text: any): any {
      return text;
    }
    
    const result = logText("Hello");
    • any를 사용하면 어떤 값이 들어올지 알 수 없고
    • 반환 타입도 any자동완성과 타입체크 불가능

    → 타입스크립트의 장점이 사라짐

    제네릭 사용

    function logText<T>(text: T): T {
      return text;
    }
    
    const r1 = logText<string>("Hello");  // 타입 명시
    const r2 = logText("Hello");          // 타입 추론
    • 함수 사용 시점에 타입 결정
    • 들어온 타입(T)을 그대로 반환 타입에도 반영

    타입 안전성 + 유연함 유지


  1. 유니온 타입보다 더 일관된 재사용하기 위해

    유니온 타입

    function toArray(a: number | string, b: number | string): (number | string)[] {
      return [a, b];
    }
    
    toArray(1, 2);        // 숫자만
    toArray("1", "2");    // 문자열만
    toArray(1, "2");      // 숫자 + 문자열 섞임
    • 타입이 커질수록 가독성도 떨어지고 관리가 어려움
    • 제약도 애매해짐

    제네릭

    function toArray<T>(a: T, b: T): T[] {
      return [a, b];
    }
    
    toArray<number>(1, 2);        // T = number → number[]
    toArray<string>("1", "2");    // T = string → string[]
    toArray<string | number>(1, "2"); // T = string | number → (string | number)[]
    toArray<number>(1, "2");      // Error
    • 호출할 때마다 같은 타입을 강제
    • 타입이 서로 다르면 컴파일 단계에서 오류 발생

    일관성 있는 타입 보장

    • 타입 추론
      toArray(1, 2);        // T = number
      toArray("1", "2");    // T = string
      toArray(1, "2");      // Error

💡 제네릭 클래스

class GenericMath<T> {
  pi!: T;
  sum!: (x: T, y: T) => T;
}

const math = new GenericMath<number>();
math.pi = 3.14;
math.sum = (x, y) => x + y;

💡 제네릭 제약

  • 타입에 제한을 두고 싶을 때 extends 사용
    interface MyType<T extends string | number> {
      name: string;
      value: T;
    }
    
    const dataA: MyType<string> = {
      name: "Data A",
      value: "Hello world",
    };
    
    const dataB: MyType<number> = {
      name: "Data B",
      value: 1234,
    };
    
    const dataC: MyType<boolean> = {
      // Error: boolean은 string | number에 해당하지 않음
      name: "Data C",
      value: true,
    };
    • T는 반드시 string 또는 number여야 함
    • 타입 별칭에도 똑같이 쓸 수 있음
      type Primitive = string | number | boolean;
      
      type MyType2<T extends Primitive> = {
        name: string;
        value: T;
      };
  • 특정 구조를 가진 타입만 허용
    function logLength<T extends { length: number }>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    
    logLength("Hello");  // 5
    logLength([1, 2, 3]);  // 3
    • T는 반드시 length 속성을 가지고 있어야 함

💡 keyof + 제네릭 - 객체 속성만 접근하도록 제한

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let obj = { a: 1, b: 2, c: 3 };

getProperty(obj, "a");  // OK
getProperty(obj, "c");  // OK
getProperty(obj, "z");  // Error: "z"는 "a" | "b" | "c"에 포함되지 않음
  • keyof T : T 타입의 모든 키들의 유니온 타입 ("a" | "b" | "c" 등)
  • K extends keyof T : K는 반드시 T의 키 중 하나
  • 결과적으로 실제 객체에 존재하는 속성만 접근할 수 있게 됨
  • 존재하지 않는 키를 컴파일 시점에 차단

💡 맵드 타입 (Mapped Type)

  • 기존 타입을 기반으로 각 속성에 동일한 규칙을 적용하여 새로운 타입을 생성하는 문법

왜 사용하나?

  • 기존 타입을 변형해 새로운 타입을 쉽게 만들 수 있음

  • 중복된 타입 정의를 줄일 수 있음

  • 타입스크립트 내장 유틸리티 타입의 기반 기능 (Readonly, Partial, Required, Record 등)

  • 예시: Readonly 타입

    type Readonly<T> = {
      readonly [P in keyof T]: T[P];
    };
    type Person = {
      name: string;
      age: number;
    };
    
    type ReadonlyPerson = Readonly<Person>;

    결과

    {
      readonly name: string;
      readonly age: number;
    } 
    • [P in keyof T]: T의 모든 키를 하나씩 순회(name, age 등)
    • readonly: 각 속성을 읽기 전용으로 변경
    • T[P]: 기존 속성 타입 그대로 사용
  • 예시: Optional 타입

    type Optional<T> = {
      [P in keyof T]?: T[P];
    };
    type Person = {
      name: string;
      age: number;
    };
    
    type OptionalPerson = Optional<Person>

    결과

    {
      name?: string;
      age?: number;
    }
    • 각 프로퍼티 뒤에 ?가 붙어 모든 속성이 선택적(optional)이 됨

💡 제네릭 함수 - merge()

  • 두 객체를 합치는 함수, 입력 타입에 따라 반환 타입이 결정됨
    function merge<T, U>(obj1: T, obj2: U): T & U {
      return { ...obj1, ...obj2 };
    }
    
    const mergedObj = merge({ name: "Alice" }, { age: 25 });
    console.log(mergedObj);
    // { name: "Alice", age: 25 }
    • <T, U>: 두 종류의 타입 변수를 받음
    • obj1: T, obj2: U: 서로 다른 타입을 허용
    • 반환 타입 T & U: 인터섹션 타입, 즉 두 타입을 합친 타입

💡 React에서 사용하기

  • React Hooks에서도 제네릭을 사용할 수 있음. 대표적으로 useState
    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;
    이렇게 useState 뒤에 <> , number타입을 넣으면 counter, setCounter에 각각 number를 인자로 받는 함수로 받는 함수 타입이 할당됨

📌 유틸리티 타입

🔗 https://www.typescriptlang.org/docs/handbook/utility-types.html

  • 타입 변환을 쉽게 하기 위해서 타입스크립트에서 제공하는 타입 도구
  • 기존 타입을 재사용 + 변형해서 효율적인 타입 설계 가능

💡 Pick<T, K>

  • T에서 특정 속성 K만 선택해 새로운 타입 생성
    interface Todo {
      id: number;
      title: string;
      description: string;
      completed: boolean;
    }
    
    type TodoPreview = Pick<Todo, "title" | "completed">;
    
    const todo: TodoPreview = {
      title: "Clean room",
      completed: false,
    };
    • 필요한 속성만 골라 쓰고 싶을 때

💡 Omit<T, K>

  • Pick과 반대
  • T에서 특정 속성 K만 제외하고 새로운 타입 생성
    interface Todo {
      id: number;
      title: string;
      description: string;
      completed: boolean;
    }
    
    type TodoWithoutDesc = Omit<Todo, "description">;
    
    const todo: TodoWithoutDesc = {
      id: 1,
      title: "Clean room",
      completed: false,
    };
    • 이거만 빼고 다 필요할 때

💡 Exclude<T ,U>

  • 유니온 타입 T에서 U를 제거
    type T0 = Exclude<"a" | "b" | "c", "a">;          // "b" | "c"
    type T1 = Exclude<"a" | "b" | "c", "a" | "b">;    // "c"
    type T2 = Exclude<string | number | (() => void), Function>; 
    // string | number
    • 특정 타입만 빼고 싶을 때

💡 Extract<T ,U>

  • Exclude와 반대
  • 유니온 타입의 교집합만 추출
    type E0 = Extract<"a" | "b" | "c", "a" | "f">;               // "a"
    type E1 = Extract<string | number | (() => void), Function>; // () => void
    • 공통되는 타입만 뽑기

💡 Partial< T>

  • T의 모든 속성을 선택적(?)으로 변경
    interface Todo {
      title: string;
      description: string;
    }
    
    function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
      return { ...todo, ...fieldsToUpdate };
    }
    
    const todo1: Todo = {
      title: "organize desk",
      description: "clear clutter",
    };
    
    const todo2 = updateTodo(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의 모든 속성을 읽기 전용으로 변경(readonly)
    interface Todo {
      title: string;
    }
    
    const todo: Readonly<Todo> = {
      title: "Delete inactive users",
    };
    
    todo.title = "Hello"; // Error: 읽기 전용 프로퍼티에 재할당할 수 없습니다.
    • 상태 보호, 불변성 유지

💡 Recode<K, T>

  • key 집합 K에 대해 모든 값이 타입 T를 가지는 객체 생성
    interface PageInfo {
      title: string;
    }
    
    type Page = "home" | "about" | "contact";
    
    const pages: Record<Page, PageInfo> = {
      home: { title: "home" },
      about: { title: "about" },
      contact: { title: "contact" },
    };
    • 미리 정의된 키를 가진 맵 객체 타입 만들 때

💡 ReturnType< T>

  • 함수 타입 T반환 타입 추론
    function getUser() {
      return { name: "Alice", age: 25 };
    }
    
    type User = ReturnType<typeof getUser>;
    
    const user: User = { name: "Alice", age: 25 };
    • 함수가 변경되어도 타입 자동 추적

💡 Parameters< T>

  • 함수 타입 T매개변수 타입들을 튜플로 만들어 줌
    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!
    • 래퍼 함수, 콜백 전달 시 유용

💡 Awaited< T>

  • Promise 내부의 실제 결과 타입 추출
    async function fetchData(): Promise<string> {
      return "Hello, world!";
    }
    
    type FetchDataType = Awaited<ReturnType<typeof fetchData>>;
    
    const data: FetchDataType = await fetchData(); // data: string
    • async/await 환경에서 타입 정확히 추적

💡 ReturnType< T> + Awaited< T>

async function exampleFunction(): Promise<number> {
  return 42;
}

type ExampleReturn = ReturnType<typeof exampleFunction>;   // Promise<number>
type ExampleResolved = Awaited<ExampleReturn>;             // number
  • ReturnType으로 함수 반환 타입을 가져오고
  • Awaited로 실제 값 타입까지 뽑아낼 수 있음

💡 정리

유틸리티 타입역할
Pick필요한 속성만 선택
Omit특정 속성 제외
Exclude유니온에서 제거
Extract유니온에서 추출
Partial모든 속성 optional
Readonly모든 속성 readonly
Record키-값 맵 타입 생성
ReturnType함수 반환 타입 추론
Parameters매개변수 타입 튜플
AwaitedPromise 결과 타입

📌 제네릭 + 유틸리티 타입 응용

💡 하나의 타입으로 여러 타입 파생시키기

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

type TodoId = Pick<Todo, "id">;

type CreateTodo = Pick<Todo, "title" | "completed">;
// type CreateTodo = Omit<Todo, "id">;

type ToggleTodo = Pick<Todo, "id" | "completed">;
// type ToggleTodo = Omit<Todo, "title">;

type EditTodo = Partial<Todo> & TodoId;

0개의 댓글