Javascript

실무에서 바로 쓸 수 있는 TypeScript 타입 8선

드리프트2 2024. 11. 9. 12:01

실무에서 바로 쓸 수 있는 TypeScript 타입 8선

이번 글에서는 실무에서 바로 적용할 수 있는 TypeScript의 주요 타입들을 소개합니다.

 

가능한 한 실용적인 예시를 사용해 설명을 진행할 건데요, React를 전제로 하고 있으니 참고해 주세요.

 

TypeScript 초보자분들에게도 도움이 될 수 있는 내용들이니 끝까지 함께 보시죠.


1. 템플릿 리터럴 타입

템플릿 리터럴 타입은 타입 안전성을 유지하면서도 문자열 조작을 할 수 있는 강력한 기능입니다.

 

예시
리터럴 타입을 사용해 문자열을 정의하면, 전달된 값이 number 타입임을 타입 시스템이 보장해줍니다.

const requestExternalApi = async ({
  data,
}: {
  data: {
    offset: `${number}`
    limit: `${number}`
  }
}) => {
  const res = await fetch("https://example.com", {
    method: "POST",
    body: JSON.stringify(data),
  })
  return res.json()
}

requestExternalApi({
  data: {
    offset: `${0}`, // 만약 `zero`를 전달하면 타입 에러 발생
    limit: `${10}`, // 마찬가지로 `ten`을 전달하면 타입 에러 발생
  },
})

2. 제네릭(Generic)

제네릭은 타입을 매개변수로 받아 재사용 가능한 컴포넌트나 함수 등을 만들 때 유용합니다.

 

예시
React의 useState와 함께 제네릭을 적용한 드롭다운 선택 컴포넌트를 만들어봅시다.

import { useState } from "react"

const Select = <T extends string>({
  options,
  value,
  onChange,
}: {
  options: T[]
  value: T
  onChange: (value: T) => void
}) => {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value as T)}>
      {options.map((option) => (
        <option key={option} value={option}>
          {option}
        </option>
      ))}
    </select>
  )
}

type Drink = "water" | "coffee"
const options: Drink[] = ["water", "coffee"]

const Component = () => {
  const [selectedOption, setSelectedOption] = useState<Drink>(options[0])

  return (
    <Select
      options={options}
      value={selectedOption}
      onChange={(val) => setSelectedOption(val)}
    />
  )
}

 

labelvalue를 분리하고 싶은 경우는 이렇게 할 수 있습니다:

import { useState } from "react"

const Select = <T extends string, U extends { label: string; value: T }>({
  options,
  value,
  onChange,
}: {
  options: U[]
  value: T
  onChange: (value: T) => void
}) => {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value as T)}>
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  )
}

type Drink = "water" | "coffee"
const userOptions: { value: Drink; label: string }[] = [
  { value: "water", label: "물" },
  { value: "coffee", label: "커피" },
]

const Component = () => {
  const [selectedOption, setSelectedOption] = useState(userOptions[0].value)

  return (
    <Select
      options={userOptions}
      value={selectedOption}
      onChange={(val) => setSelectedOption(val)}
    />
  )
}

3. 유니온 타입(|)과 인터섹션 타입(&)

유니온 타입은 여러 타입 중 하나를 사용할 수 있게 하고, 인터섹션 타입은 여러 타입을 조합해 사용합니다.

 

예시
개인 사용자와 회사 사용자를 구분하는 데이터를 처리하는 방법을 살펴봅시다.

type BasicInfo = {
  id: number
  name: string
}

type PersonalUser = BasicInfo & {
  type: "personal"
  age: number
}

type CompanyUser = BasicInfo & {
  type: "company"
  phone: string
}

type User = PersonalUser | CompanyUser

const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch("https://api.example.com/users")
  if (!response.ok) {
    throw new Error("Failed to fetch users")
  }

  return (await response.json()) as User[]
}

const Component = async () => {
  const users = await fetchUsers()

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>
          <div>{user.name}</div>
          {user.type === "personal" && <div>{user.age}</div>}
          {user.type === "company" && <div>{user.phone}</div>}
        </div>
      ))}
    </div>
  )
}

4. Mapped Types와 as

Mapped Types를 사용하면 기존 타입의 프로퍼티 이름을 유연하게 변환할 수 있습니다.

 

예시

import type { KebabCase } from "type-fest"

type GameSearchState = {
  gameId: number
  nameQuery: string
  isSoldOut: boolean
}

type UrlTypes<T> = {
  [K in keyof T as KebabCase<K>]: string
}

type UrlTypeGameSearchState = UrlTypes<GameSearchState>

const search = (state: UrlTypeGameSearchState) => {
  const url = new URL("https://example.com/search")
  Object.entries(state).forEach(([key, value]) => {
    url.searchParams.set(key, value)
  })
  return (window.location.href = url.toString())
}

const Component = () => {
  const [state, setState] = useState<GameSearchState>({
    gameId: 1,
    nameQuery: "Alice",
    isSoldOut: true,
  })

  const handleSearch = () => {
    search({
      "game-id": state.gameId.toString(),
      "name-query": state.nameQuery,
      "is-sold-out": state.isSoldOut.toString(),
    })
  }
}

5. 조건부 타입 (T extends U ? X : Y)

조건부 타입은 주어진 조건에 따라 타입을 분기할 수 있습니다.

 

예시

const extractNumFields = <T extends Record<string, string | number>>(
  response: T,
): { [K in keyof T as T[K] extends number ? K : never]: T[K] } => {
  // ...
}

const aaa = extractNumFields({ a: 1, b: "2" }) // => { a: number }

6. ReturnTypeParameters

이 두 유틸리티 타입은 함수의 반환값 타입과 파라미터 타입을 재정의 없이 재사용할 수 있게 해줍니다.

 

예시

const requestToExternalApi = async () => {
  const response = await fetch("https://api.example.com/users")
  if (!response.ok) {
    throw new Error("Failed to fetch users")
  }
  return response.json() as Promise<{
    id: number
    type: "user"
  }>
}

const convertFromExternalApi = (
  response: Awaited<ReturnType<typeof requestToExternalApi>>,
) => {
  return {
    id: response.id,
    myType: response.type,
  }
}

7. as constsatisfies

as constsatisfies를 함께 사용하면 리터럴 타입을 유지하면서도 타입 체크를 강화할 수 있습니다.

 

예시

const localizeData_JP = {
  novel: {
    chapter: "챕터",
    episode: "화",
  },
} as const satisfies {
  novel: {
    chapter: string
    episode: string
  }
}

const chapter = localizeData_JP.novel.chapter // => "챕터"

8. 재귀적 타입

재귀적인 데이터 구조를 다루는 것은 까다로울 수 있지만, TypeScript로 잘 처리할 수 있습니다.

 

예시

const localizeData_JP = {
  novel: {
    chapter: "챕터",
    episode: "화",
  },
} as const satisfies {
  novel: {
    chapter: string
    episode: string
  }
}

type RecursiveObj = { [key: string]: string | RecursiveObj }

type UnionWithDot<
  Prefix extends string,
  Key,
> = `${Prefix}${Prefix extends "" ? "" : "."}${Key extends string ? Key : ""}`

type DotKeys<Obj extends RecursiveObj, Prefix extends string = ""> = {
  [Key in keyof Obj]: Obj[Key] extends object
    ? DotKeys<Obj[Key], UnionWithDot<Prefix, Key>>
    : UnionWithDot<Prefix, Key>
}[keyof Obj]

export type ExtractedDotKeys = DotKeys<typeof localizeData_JP>

const localize = (key: ExtractedDotKeys) => {
  const splittedKey = key.split(".")

  const stringOrObj = splittedKey.reduce<RecursiveObj | string>((obj, key) => {
    if (typeof obj === "string") return obj

    return obj[key]
  }, localizeData_JP)

  return stringOrObj
}

localize("novel.chapter") // 만약 "novel.chap"으로 입력하면 타입 에러 발생

 

위 코드는 재귀적으로 객체를 탐색해 "."으로 구분된 문자열 키 값을 찾아주는 함수입니다.

 

잘못된 문자열을 입력할 경우 TypeScript가 즉시 타입 에러를 발생시켜 실수를 방지할 수 있습니다.


결론

TypeScript의 다양한 타입 시스템을 잘 활용하면, 코드의 안정성과 유지 보수성을 크게 높일 수 있습니다.

 

오늘 소개한 8가지 타입은 실무에서 자주 활용될 만한 것들인데요, 타입스크립트를 처음 접하는 분들도 부담 없이 따라 할 수 있도록 예시를 중심으로 설명드렸습니다.

 

앞으로 TypeScript와 더 친해지고 싶다면, 다양한 타입들과 계속 마주하며 학습하는 게 중요합니다.

 

타입스크립트의 강력한 타입 시스템을 잘 활용해 더 안전하고 효율적인 코드를 작성해 보세요!