실무에서 바로 쓸 수 있는 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)}
/>
)
}
label
과 value
를 분리하고 싶은 경우는 이렇게 할 수 있습니다:
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. ReturnType
과 Parameters
이 두 유틸리티 타입은 함수의 반환값 타입과 파라미터 타입을 재정의 없이 재사용할 수 있게 해줍니다.
예시
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 const
와 satisfies
as const
와 satisfies
를 함께 사용하면 리터럴 타입을 유지하면서도 타입 체크를 강화할 수 있습니다.
예시
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와 더 친해지고 싶다면, 다양한 타입들과 계속 마주하며 학습하는 게 중요합니다.
타입스크립트의 강력한 타입 시스템을 잘 활용해 더 안전하고 효율적인 코드를 작성해 보세요!
'Javascript' 카테고리의 다른 글
TypeScript의 불편함을 해결하는 @total-typescript/ts-reset 도입하기 (1) | 2024.11.09 |
---|---|
reduce로 날짜별 정보 배열 만들기 (0) | 2024.11.09 |
자바스크립트 얕은 복사 vs 깊은 복사, 완벽 정리! (0) | 2024.10.30 |
Next.js 15, 달라진 점 싹 다 훑어보기! (1) | 2024.10.30 |
Next.js "use cache" 한방에 이해하기! (0) | 2024.10.30 |