interface MathFn {
  (a: number, b: number): number
}
const sum: MathFn = (a, b) => a + b

Footnotes

  1. function declarations, function expressions, arrow functions, methods등 TypeScript에서 함수를 선언하는 다양한 방법들.

#15
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')
)

#255
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}`)
}

#275

두 개의 오버로딩된 메서드에서 각각의 반환 타입을 명시적으로 추출하기

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>
#345

템플릿 리터럴 타입을 활용하여 타입 정의하기

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>
  )
}
#350
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로 구성

#376

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()
  }
}

단순 초기화만 필요하면 일반 생성자가 더 직관적. 복잡한 생성 로직에만 사용.

#381

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
#382

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)
      }}
    />
  )
}
#480

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 } }
#499

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"
}

TS 버전 올리면서 target 그대로 두면 Webpack4 같은 구형 번들러에서 파싱 실패할 수 있음.

#501

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
}

가장 자주 사용되는 케이스를 기본값으로 설정하면 제네릭을 항상 명시하는 번거로움 줄어듦.

#516

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로 자동 생성
#524

간단한 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
    },
  }
}
#529

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
}
#533

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 {}
#535

여러 단계의 비동기 UI 업데이트를 setLoading → setProgress → setLoading(false)로 흩뿌리지 않고 async generator의 yield 시퀀스로 시간 순서대로 표현. 원본 트릭은 @ericclemmons.

type FlowEvent<P extends string, D> = { phase: P; data: D }

async function* loadingFlow(
  signal: AbortSignal,
): AsyncGenerator<FlowEvent<'starting' | 'slow' | 'done', string>> {
  yield { phase: 'starting', data: 'Starting…' }
  await wait(1000, signal)
  yield { phase: 'slow', data: 'Taking longer than usual' }
  await wait(2000, signal)
  yield { phase: 'done', data: 'Got it 🎉' }
}

원본의 빈 자리 → 소비 레이어 책임

원본 데모는 4가지가 비어 있음. hook이든 actor든 wrapping 레이어에서 채워야 함:

  • cancellation: generator에 AbortSignal 주입 → await 대상(wait, fetch)이 signal-aware해야 진짜 취소됨
  • 재진입: 새 실행 시 직전 iterator abort
  • error path: generator 내부 throw → 상태 노출
  • unmount/teardown: cleanup으로 in-flight iterator abort
  • stale closure: 최신 클로저 참조 (hook은 ref, actor는 input)
  • typed: phase/data 모두 좁힘

어디에 맞는가

generator는 시간이 코드의 한 방향(↓)으로만 흐르는 모델. 그 모양에 맞는 시나리오:

  • 멀티 단계 loading 메시지 (위 loadingFlow)
  • progressive search — tier별로 점진적 결과 yield, tier 경계가 자연스러운 cancellation point, debounce 불필요
  • optimistic mutation — { phase: 'optimistic' | 'reconciled' | 'rolledBack', data } 전이를 4개 콜백 대신 한 함수로
#540

WeakMap의 키를 “입력 배열의 참조 자체”로 쓰면 캐시 무효화 로직이 0줄이 된다 — 무효화를 참조 동등성에 위임.

const indexCache = new WeakMap<Item[], Map<string, string>>()

// name → id 역인덱스를 1회 빌드 (O(n))
const buildIndex = (items: Item[]) =>
  new Map(items.map((it) => [it.name, it.id]))

function lookup(items: Item[], name: string): string | null {
  let index = indexCache.get(items)
  if (!index) {
    index = buildIndex(items) // 캐시 미스일 때만 빌드
    indexCache.set(items, index)
  }
  return index.get(name) ?? null
}
  • 같은 참조로 재호출 → cache hit, O(1)
  • 데이터가 새 참조로 교체(refetch 등) → 자동 miss → 새 index 빌드. 옛 index는 옛 items와 함께 GC 대상
  • 키가 weak reference라 items가 어디서도 안 잡히면 entry도 같이 사라짐. 일반 Map이면 참조가 영구히 붙들려 메모리 누수

전제: 호출 측 items 참조가 stable해야 작동. 매번 [...data]·data.filter()로 새 배열을 넘기면 항상 miss라 무의미. (TanStack Query의 data/select 결과는 참조 안정성을 보장하므로 그대로 넘기면 OK)

참고

prop 프리셋을 데이터로 중앙화하는 타입 헬퍼를, 가로축 excess(프로퍼티명 오타) 검출 · 세로축 개별 리터럴 보존으로 좌표에 놓으면 단일 named 헬퍼로 네 칸을 다 차지하는 점은 없다가 한눈에 보인다.

타입 헬퍼 4종을 excess 검출(가로) × 개별 리터럴 보존(세로) 사분면에 배치 — 우상단(둘 다 충족)을 named 헬퍼로 차지하는 점은 없음

우상단(둘 다 ✓)엔 satisfies와 Exact-never가 있지만, 각각 “어노테이션 반복” / “메시지 나쁨”을 대가로 치른다.

type TextStyleProps = Omit<TextProps, 'children'>

// ① 제네릭 항등 — 리터럴 O, excess 검출 X (조용히 T로 흡수)
const createTextStyles = <T extends Record<string, TextStyleProps>>(s: T): T =>
  s

// ② 키 제네릭 — excess O(메시지 좋음), 키 O, 개별 리터럴은 유니온으로 넓어짐  ← 추천
const createTextStyles = <K extends string>(
  s: Record<K, TextStyleProps>,
): Record<K, TextStyleProps> => s

// ③ Exact-never — 리터럴·excess O, 단 에러가 "not assignable to never"
type Exact<V> = {
  [P in keyof V]: P extends keyof TextStyleProps ? TextStyleProps[P] : never
}
const createTextStyles = <T extends Record<string, TextStyleProps>>(
  s: T & { [K in keyof T]: Exact<T[K]> },
): T => s

// ④ 인라인 satisfies — 리터럴·excess·메시지 다 O, 단 named 헬퍼 아님 + 어노 반복
const guideStyles = {
  title: { type: 'body1_700', color: COLORS.GRAY_90 },
} satisfies Record<string, TextStyleProps>

값 오타(type: 'nope')·키 오참조(styles.titel)는 ①②③④ 전부 잡는다. 갈리는 건 프로퍼티명 오타개별 type 리터럴뿐.

구조적 이유: excess 검출은 고정 target을, 리터럴 보존은 *제네릭 캡처(T)*를 요구해 서로 반대로 당긴다. satisfies만 컴파일러가 특별 취급해 둘을 한 번에 주지만, 그 대가가 “구문이지 헬퍼가 아님”이다.

권장: 엄격성이 목표면 ② (잃는 건 거의 안 쓰는 개별 type 리터럴). 개별 리터럴이 진짜 필요하면 ④. color가 required면 ①도 프로퍼티명 오타가 “필수 누락”으로 걸려 갭이 거의 사라진다.

프리셋을 컴포넌트가 아니라 prop 데이터로 다루면 래퍼 컴포넌트 비용(displayName·리마운트·추가 fiber)을 회피하고, override는 평범한 JSX 스프레드로 투명하다.

#566

날짜 표기를 통일하는 데 매핑 객체가 필요 없다 — dayjs는 포맷 문자열이 곧 키이자 값이라, 허용 표기를 as const 배열 하나로 두면 그게 닫힌 집합이다. 닫힘은 union 타입이, 통로는 함수 하나가 맡는다. (i18n은 키≠값이라 객체가 필수였지만, 여기선 잉여.)

import dayjs from 'dayjs'

// 허용 표기의 단일 출처 — union 타입과 ESLint drift 가드가 여기서 파생
export const DATE_FORMATS = [
  'YYYY-MM-DD',
  'YYYY-MM-DDTHH:mm:ss',
  'HH:mm',
  'YYYY년 M월',
  'YY년 M월 D일',
  'YYYY.MM.DD',
  'YY.MM.DD',
  'YYYY.MM.DD(ddd)',
  'YYYY.MM.DD HH:mm',
] as const

export type DateFormat = (typeof DATE_FORMATS)[number]

export const formatDate = (
  date: dayjs.ConfigType,
  format: DateFormat,
): string => dayjs(date).format(format)

규율: 여러 곳에서 통일이 필요한 표기만 배열에 담는다. 일회성('M.D' 등)은 raw dayjs().format()으로 인라인 유지 — 안 그러면 배열이 잡동사니 enum이 된다. ESLint no-restricted-syntax로 카탈로그 표기를 raw로 다시 쓰면 경고(drift 가드).

ddd(요일)는 진입점에서 dayjs.locale('ko')를 호출해야 한국어로 나온다 — 안 하면 영어로 조용히 샌다. 타입은 이 런타임 전제를 못 잡는다.

곁가지 — 매핑 객체·빌드타임 hoist·tagged-template 캐시도 검토했으나 접음: 고유 패턴이 수십 개 규모라 dedup·컴파일 최적화가 풀 문제 자체가 없다. 닫힌 타입 + 함수 하나로 충분.

#576