interface MathFn {
(a: number, b: number): number
}
const sum: MathFn = (a, b) => a + b
Footnotes
-
function declarations, function expressions, arrow functions, methods등 TypeScript에서 함수를 선언하는 다양한 방법들. ↩
const { Parser } = require('acorn')
const JSXParser = Parser.extend(require('acorn-jsx')())
const isReactComponent = Boolean(
JSON.stringify(
JSXParser.parse(fileContent, {
sourceType: 'module',
ecmaVersion: 'latest',
})
).includes('JSXIdentifier')
)
import { match } from 'ts-pattern'
type Format = 'webp' | 'jpg'
type Params = {
id: string
quality: keyof typeof QUALITY_MAP
format: Format
}
const QUALITY_MAP = {
player_background: '0',
video_frames_start: '1',
video_frames_middle: '2',
video_frames_end: '3',
lowest_quality: 'default',
medium_quality: 'mqdefault',
high_quality: 'hqdefault',
standard_quality: 'sddefault',
unscaled_resolution: 'maxresdefault',
}
const BASE_URL = 'https://i.ytimg.com'
const VI = (format: Format) =>
match(format)
.with('jpg', () => 'vi')
.otherwise(() => ['vi', format].join('_'))
export function getThumbnail({ id, quality, format }: Params) {
return [BASE_URL, VI(format), id, QUALITY_MAP[quality]]
.join('/')
.concat(`.${format}`)
}
button 또는 a 요소를 선택적으로 렌더링할 수 있는 ButtonOrLink 컴포넌트를 구현한 부분. props로 전달된 as 값에 따라 해당 태그를 렌더링하며, React.JSX.IntrinsicElements의 타입을 활용하여 각 태그에 맞는 props를 받을 수 있도록 한다.
import * as React from 'react'
type ComponentPropsWithAs<T extends keyof React.JSX.IntrinsicElements> = {
as: T
} & React.ComponentProps<T>
type ButtonOrLinkProps =
| ComponentPropsWithAs<'a'>
| ComponentPropsWithAs<'button'>
export function ButtonOrLink(props: ButtonOrLinkProps) {
switch (props.as) {
case 'a':
return <a {...props} />
case 'button':
return <button {...props} />
default:
return null
}
} 두 개의 오버로딩된 메서드에서 각각의 반환 타입을 명시적으로 추출하기
type Year = {
year(): number
year(u: string): string
}
type ReturnTo<T, R> = T extends R ? R : never
type GetYear = ReturnTo<Year['year'], () => number>
type SetYear = ReturnTo<Year['year'], (u: string) => string> 템플릿 리터럴 타입을 활용하여 타입 정의하기
import * as React from 'react'
type RenderPropNames = 'Title' | 'Content' | 'Actions'
type RenderProps = {
[K in RenderPropNames as `render${K}`]: () => React.ReactNode
}
type Props = RenderProps
/**
* @example
*
* ```tsx
* <DialogComponent
* renderTitle={() => <h2>Title</h2>}
* renderContent={() => <p>Content</p>}
* renderActions={() => (
* <div>
* <button onClick={handleClose}>Close</button>
* <button onClick={handleSubmit}>Submit</button>
* </div>
* )}
* />
* ```
*/
function Dialog({
renderTitle,
renderContent,
renderActions,
}: Props) => {
return (
<div data-scope="root">
<div data-part="content">
{renderTitle()}
{renderContent()}
</div>
<div data-part="actions">{renderActions()}</div>
</div>
)
} type JsonPrimitive = string | number | boolean | null
type JsonObject = { [Key in string]: JsonValue } & {
[Key in string]?: JsonValue | undefined
}
type JsonArray = JsonValue[] | readonly JsonValue[]
type JsonValue = JsonPrimitive | JsonObject | JsonArray
예전에 JSON 타입 정의가 필요해서 찾아봤던 내용
JsonObject: 문자열 키와 JsonValue 타입의 값을 가진 JSON 객체를 정의JsonArray: JsonValue 타입의 요소를 포함하는 JSON 배열을 정의JsonPrimitive: 문자열, 숫자, 불린, 또는 null과 같은 유효한 JSON 기본 값을 정의JsonValue: 유효한 JSON 값을 나타내며, JsonPrimitive, JsonObject, 또는 JsonArray로 구성
static create 패턴 - 생성자 대신 정적 메서드로 객체 생성
언제 사용?
- 생성 로직이 복잡하거나 유효성 검사 필요
- 생성 실패 시 null/Result 반환 (생성자는 항상 인스턴스 반환)
- 팩토리 패턴, 싱글톤
class User {
private constructor(private readonly name: string) {}
// 유효성 검사 + 실패 시 null 반환
static create(name: string): User | null {
if (!name || name.length < 3) return null
return new User(name)
}
}
// 팩토리 패턴
class Shape {
static create(type: 'circle' | 'rect', size: number): Shape {
return type === 'circle' ? new Circle(size) : new Rectangle(size)
}
}
// 싱글톤
class Config {
private static instance: Config
private constructor() {}
static create() {
return Config.instance ??= new Config()
}
}
TIP
단순 초기화만 필요하면 일반 생성자가 더 직관적. 복잡한 생성 로직에만 사용.
Figma의 권한 관리 DSL - JSON 직렬화 가능한 DSL로 정책 표현, TypeScript 기반 평가 엔진 구현.
기존 문제: 불필요한 복잡성, 계층적 권한 비효율, DB 부하, 여러 진실 소스
type ExpressionDef = BinaryExpressionDef | OrExpressionDef | AndExpressionDef
// 바이너리 표현식: [필드, 연산자, 값]
const binaryExpression = ['file.id', '<>', null] satisfies ExpressionDef
// AND/OR 조합
const andExpression = {
and: [
['file.id', '<>', null],
['team.permission', '=', 'open'],
],
} satisfies ExpressionDef React 제네릭 컴포넌트 패턴 - <T extends Record<string, any>>로 row/item 타입 추론
import * as React from 'react'
type Props<TRow> = {
rows: TRow[]
renderRow: (row: TRow, index: number) => React.ReactNode
}
const Table = <TRow extends Record<string, any>>({
rows,
renderRow,
}: Props<TRow>) => {
return (
<table>
<tbody>{rows.map((row, index) => renderRow(row, index))}</tbody>
</table>
)
}
function App() {
return (
<Table
rows={[{ name: 'lee' }]}
renderRow={(row, index) => (
<tr key={index}>
<td>{row.name}</td>
</tr>
)}
/>
)
}
import * as React from 'react'
import { UseComboboxProps, useCombobox } from 'downshift'
function Combobox<T extends Record<string, any>>(
props: UseComboboxProps<T> & {
renderItem: (item: T) => React.ReactNode
}
) {
const combobox = useCombobox(props)
return (
<div>
<input
placeholder="구성원을 검색해주세요."
{...combobox.getInputProps()}
/>
<div {...combobox.getMenuProps()}>
{combobox.isOpen &&
props.items.map((item, index) => (
<div key={item.id} {...combobox.getItemProps({ item, index })}>
{props.renderItem(item)}
</div>
))}
</div>
</div>
)
}
function App() {
return (
<Combobox
items={[{ id: 1, name: 'eunsoo' }]}
itemToString={(item) => `${item?.id}`}
renderItem={(item) => {
return <div>{item.name}</div>
}}
onInputValueChange={({ inputValue }) => {
console.log(inputValue)
}}
/>
)
} TypeScript 중첩 객체 타입 부분 수정
interface O {
actions: { a: string; b: number }
}
// 중첩 속성 Optional로 변경
type MakeNestedOptional<T, K extends keyof T, OK extends keyof T[K]> = Omit<
T,
K
> & {
[P in K]: Omit<T[K], OK> & Partial<Pick<T[K], OK>>
}
type Result = MakeNestedOptional<O, 'actions', 'b'> // { actions: { a: string; b?: number } }
// 중첩 속성 타입 오버라이드
type OverrideNested<T, K extends keyof T, Override> = Omit<T, K> & {
[P in K]: Omit<T[K], keyof Override> & Override
}
type Result2 = OverrideNested<O, 'actions', { b: boolean }> // { actions: { a: string; b: boolean } }
한두 군데만 쓸 거면 그냥 손으로 타입 작성이 더 명확함. 유틸리티 타입은 반복 사용할 때만 가치.
// 라이브러리 객체 변이 주의
// ❌ info.actions.onDownload = undefined
// ✅ const modifiedInfo = { ...info, actions: { ...info.actions, onDownload: undefined } } tsconfig 핵심: target, lib, module
TypeScript (.ts)
↓
lib: 타입 체크 시 뭘 알고 있나? (Promise, Map 등)
↓
target: 문법을 얼마나 낮출 건가? (ES2020 → ?. 그대로, ES2019 → 삼항연산자로)
↓
module: import/export를 뭘로? (CommonJS → require, ESNext → import)
↓
JavaScript (.js)
target vs lib 분리 이유: 문법(syntax)과 API(runtime)는 다름
?.→ target이 변환 (문법)Promise→ 폴리필이 해결 (API)
{
"target": "ES2019", // 하위호환 (optional chaining 이전)
"module": "ESNext", // 트리쉐이킹 가능
"lib": ["ES2020", "DOM"], // 타입은 넉넉하게
"moduleResolution": "Node"
}
CAUTION
TS 버전 올리면서 target 그대로 두면 Webpack4 같은 구형 번들러에서 파싱 실패할 수 있음.
TypeScript 제네릭 기본값 - <T extends Type = DefaultValue>
type SelectProps<T extends 'single' | 'multiple' = 'multiple'> = {
mode: T
value: T extends 'single' ? string : string[]
}
const a: SelectProps = { mode: 'multiple', value: ['a', 'b'] } // 기본값 사용
const b: SelectProps<'single'> = { mode: 'single', value: 'a' } // 명시적 지정
// 여러 매개변수에 각각 기본값
type ApiResponse<
TData = any,
TStatus extends 'loading' | 'success' | 'error' = 'loading'
> = {
data: TStatus extends 'success' ? TData : null
status: TStatus
}
가장 자주 사용되는 케이스를 기본값으로 설정하면 제네릭을 항상 명시하는 번거로움 줄어듦.
AsChild 패턴 (Radix UI)
컴포넌트 기능은 유지하면서 렌더링 요소를 변경:
<Button asChild>
<a href="/home">Link Button</a>
</Button>
핵심 구현 - Slot 컴포넌트:
const Slot = forwardRef(({ children, ...props }, ref) => {
if (!isValidElement(children)) return null
return cloneElement(children, {
...props,
...children.props,
ref: ref || children.ref,
})
})
자식 요소 타입에 따른 조건부 Props:
type ConditionalProps<T> = T extends ReactElement<any, 'a'>
? { href?: string; external?: boolean }
: T extends ReactElement<any, 'button'>
? { type?: 'button' | 'submit' }
: {}
// 사용
;<Anchor href="/home" external>
{' '}
{/* a 태그일 때만 href 허용 */}
<a>Link</a>
</Anchor>
활용: 디자인 시스템, 라우터 통합 (<Button asChild><NextLink /></Button>)
TypeScript Discriminated Union 문서화 딜레마
// 개발: 타입 안전, IDE 자동완성, 명확한 의도
type Button =
| { type: 'submit'; color: string }
| { type: 'reset'; text: string }
<!-- 문서화: 조건부 속성 설명 어려움, 테이블 복잡, 예시 다수 필요 -->
| 속성 | 타입 | 조건 |
| ----- | ------------------- | ----------------------- |
| type | 'submit' \| 'reset' | 필수 |
| color | string | type='submit'일 때 필수 |
| text | string | type='reset'일 때 필수 |
- 자세한 타입 정의는 TypeScript 정의 파일 참고
- typedoc, api-extractor로 자동 생성
간단한 Redux 스타일 Store 구현
type Reducer<S, A> = (state: S, action: A) => S
type Listener<S> = (state: S) => void
interface Store<S, A> {
getState: () => S
subscribe: (listener: Listener<S>) => () => void
dispatch: (action: A) => A
}
function createStore<S, A>(
reducer: Reducer<S, A>,
preloadedState: S
): Store<S, A> {
let currentState = preloadedState
let listeners: Listener<S>[] = []
return {
getState: () => currentState,
subscribe: (listener) => {
listener(currentState)
listeners.push(listener)
return () => {
listeners = listeners.filter((l) => l !== listener)
}
},
dispatch: (action) => {
currentState = reducer(currentState, action)
listeners.forEach((l) => l(currentState))
return action
},
}
} Maybe Monad로 null 체크 체이닝
type Maybe<T> = T | null
const Maybe = {
of: <T>(value: T): Maybe<T> => (value != null ? value : null),
map: <T, U>(m: Maybe<T>, fn: (v: T) => U): Maybe<U> =>
m != null ? Maybe.of(fn(m)) : null,
}
// 사용 예: DOM 요소 찾아서 스크롤
Maybe.of(scrollElement.current)
.map((root) => root.querySelector(`#${id}`))
.map((target) => target.getClientRects()[0])
.map((rect) => {
root.scrollLeft = rect.left
})
Generator로 early return 패턴도 가능
function* handleScroll(id) {
const root = scrollElement.current
if (!root) return
const target = root.querySelector(`#${id}`)
if (!target) return
yield target.getClientRects()[0]?.left ?? 0
} TypeScript Maybe Monad 구현
type Maybe<T> = Just<T> | Nothing
class Just<T> {
constructor(public value: T) {}
bind<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
return fn(this.value)
}
}
class Nothing {
bind<U>(fn: (value: any) => Maybe<U>): Maybe<U> {
return this
}
}
const nothing = new Nothing()
function safeDivide(x: number, y: number): Maybe<number> {
return y === 0 ? nothing : new Just(x / y)
}
new Just(10).bind((x) => safeDivide(x, 2)) // Just { value: 5 }
new Just(10).bind((x) => safeDivide(x, 0)) // Nothing {}