본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: NoInfer와 새로운 유틸리티 타입
2026년 2월 7일·프로그래밍·

9장: NoInfer와 새로운 유틸리티 타입

TypeScript 5.4의 NoInfer 유틸리티 타입과 5.x에서 추가된 새로운 타입 도구들을 활용한 라이브러리 설계 패턴을 다룹니다.

14분710자6개 섹션
typescriptperformancedevtools
공유
typescript-deepdive9 / 12
123456789101112
이전8장: infer 키워드와 타입 추론 마스터다음10장: 타입 수준 프로그래밍 — 타입으로 로직 작성하기

8장에서 infer 키워드의 고급 패턴을 다뤘습니다. 이 장에서는 infer의 반대 개념이라 할 수 있는 NoInfer를 포함한 TypeScript 5.x의 새로운 유틸리티 타입들을 살펴봅니다. 이 도구들은 라이브러리 설계에서 타입 추론의 방향과 범위를 정밀하게 제어하는 핵심 역할을 합니다.

NoInfer의 등장 배경

TypeScript의 타입 추론 엔진은 제네릭 함수의 모든 매개변수에서 타입 정보를 수집하여 타입 파라미터를 결정합니다. 대부분의 경우 이 동작은 직관적이지만, 여러 매개변수가 동일한 타입 파라미터를 공유할 때 의도하지 않은 결과가 발생할 수 있습니다.

NoInfer 이전의 문제
typescript
function createFSM<S extends string>(config: {
  initial: S;
  states: S[];
}) {
  return config;
}
 
// 의도: states에 있는 값만 initial로 사용하고 싶음
const fsm = createFSM({
  initial: "idle",
  states: ["idle", "loading", "success", "error"],
});
// S: "idle" | "loading" | "success" | "error" — OK
 
// 문제: 오타를 넣어도 에러가 발생하지 않음
const fsm2 = createFSM({
  initial: "idel",  // 오타!
  states: ["idle", "loading", "success", "error"],
});
// S: "idel" | "idle" | "loading" | "success" | "error" — "idel"이 S에 포함됨!

initial에서도 S를 추론하기 때문에, 오타가 유니온에 합류해버립니다. TypeScript 5.4 이전에는 이를 해결하기 위해 복잡한 타입 트릭이 필요했습니다.

NoInfer 유틸리티 타입

NoInfer<T>는 특정 위치에서 타입 추론이 일어나지 않도록 차단합니다. 해당 위치의 값은 다른 위치에서 추론된 타입에 대해 검증만 수행됩니다.

NoInfer 적용
typescript
function createFSM<S extends string>(config: {
  initial: NoInfer<S>;  // 이 위치에서는 S를 추론하지 않음
  states: S[];           // S는 여기서만 추론
}) {
  return config;
}
 
const fsm = createFSM({
  initial: "idle",
  states: ["idle", "loading", "success", "error"],
});
// S: "idle" | "loading" | "success" | "error" (states에서만 추론)
// initial: "idle" — S에 할당 가능하므로 OK
 
const fsm2 = createFSM({
  initial: "idel",  // Error! "idel"은 "idle" | "loading" | "success" | "error"에 할당 불가
  states: ["idle", "loading", "success", "error"],
});

NoInfer의 동작 원리

NoInfer<T>는 타입 자체를 변경하지 않습니다. NoInfer<string>은 여전히 string입니다. 차이는 타입 추론 과정에서만 나타나며, 컴파일러가 해당 위치를 추론 후보에서 제외합니다.

NoInfer는 타입을 변경하지 않음
typescript
type A = NoInfer<string>;  // string
type B = NoInfer<number>;  // number
 
// 런타임에는 아무 영향 없음
function example<T>(a: T, b: NoInfer<T>) {
  // b의 타입은 T — NoInfer가 벗겨진 상태
}

실전 패턴

기본값 제한

제네릭 함수에서 기본값이 추론을 오염시키지 않도록 합니다.

기본값 패턴
typescript
function createSignal<T>(
  initialValue: T,
  options?: {
    fallback: NoInfer<T>;
    onChange?: (value: NoInfer<T>) => void;
  }
) {
  // ...
}
 
// T는 initialValue에서만 추론
createSignal("hello", {
  fallback: "default",              // OK: string
  onChange: (v) => console.log(v),   // v: string
});
 
createSignal(42, {
  fallback: "not a number",  // Error: string은 number에 할당 불가
});

이벤트 시스템에서 페이로드 검증

이벤트 페이로드 검증
typescript
type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; code: string };
  resize: { width: number; height: number };
};
 
function on<K extends keyof EventMap>(
  event: K,
  handler: (payload: NoInfer<EventMap[K]>) => void
) {
  // 이벤트 등록...
}
 
on("click", (payload) => {
  console.log(payload.x, payload.y);  // OK
});
 
on("keydown", (payload) => {
  console.log(payload.key);  // OK
  console.log(payload.x);    // Error: 'x'는 존재하지 않음
});

테마 시스템

타입 안전 테마
typescript
function createTheme<T extends Record<string, string>>(config: {
  tokens: T;
  defaults: { [K in keyof T]?: NoInfer<T[K]> };
}) {
  return config;
}
 
const theme = createTheme({
  tokens: {
    primary: "#3b82f6",
    secondary: "#64748b",
    danger: "#ef4444",
  },
  defaults: {
    primary: "#3b82f6",    // OK
    secondary: "#64748b",  // OK
    danger: "#invalid",    // 이상적으로는 에러이지만, string이므로 통과
    // unknown: "#000",    // Error: 'unknown'은 keyof T에 없음
  },
});

라우터에서 파라미터 제한

라우트 네비게이션
typescript
type Routes = {
  "/users": {};
  "/users/:id": { id: string };
  "/posts/:postId/comments/:commentId": { postId: string; commentId: string };
};
 
function navigate<P extends keyof Routes>(
  path: P,
  params: NoInfer<Routes[P]>
) {
  // 네비게이션 로직...
}
 
navigate("/users", {});                              // OK
navigate("/users/:id", { id: "123" });               // OK
navigate("/users/:id", {});                          // Error: id가 필요
navigate("/users/:id", { id: "123", extra: true });  // Error: extra는 존재하지 않음

5.x의 다른 유틸리티 타입 개선

내장 유틸리티 타입 총정리

TypeScript가 제공하는 내장 유틸리티 타입을 카테고리별로 정리합니다.

객체 변환 유틸리티
typescript
// 프로퍼티 수정자
type Partial<T>    // 모든 프로퍼티를 선택적으로
type Required<T>   // 모든 프로퍼티를 필수로
type Readonly<T>   // 모든 프로퍼티를 readonly로
 
// 프로퍼티 선택/제외
type Pick<T, K>    // 특정 프로퍼티만 선택
type Omit<T, K>    // 특정 프로퍼티 제외
type Record<K, T>  // 키-값 쌍으로 객체 타입 생성
유니온/인터섹션 유틸리티
typescript
type Exclude<T, U>      // T에서 U에 할당 가능한 타입 제외
type Extract<T, U>      // T에서 U에 할당 가능한 타입만 추출
type NonNullable<T>     // null | undefined 제거
type NoInfer<T>         // 5.4+ — 추론 차단
함수 유틸리티
typescript
type ReturnType<T>              // 함수 반환 타입
type Parameters<T>              // 함수 매개변수 타입 (튜플)
type ConstructorParameters<T>   // 생성자 매개변수 타입
type InstanceType<T>            // 클래스 인스턴스 타입
type ThisParameterType<T>       // this 매개변수 타입
type OmitThisParameter<T>      // this 매개변수 제거
문자열 유틸리티
typescript
type Uppercase<T>      // 대문자 변환
type Lowercase<T>      // 소문자 변환
type Capitalize<T>     // 첫 글자 대문자
type Uncapitalize<T>   // 첫 글자 소문자
Promise 유틸리티
typescript
type Awaited<T>  // Promise 내부 타입 추출 (재귀적)

커스텀 유틸리티 타입 설계

내장 유틸리티 타입을 조합하여 프로젝트에 맞는 커스텀 유틸리티를 설계할 수 있습니다.

실용적 커스텀 유틸리티
typescript
// 특정 프로퍼티만 선택적으로
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
 
// 특정 프로퍼티만 필수로
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
 
// 특정 프로퍼티만 Readonly로
type ReadonlyBy<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>;
 
// 사용 예시
interface User {
  id: string;
  name: string;
  email: string;
  bio?: string;
  avatar?: string;
}
 
type CreateUser = PartialBy<User, "id">;
// { name: string; email: string; bio?: string; avatar?: string; id?: string }
 
type UserWithRequiredProfile = RequiredBy<User, "bio" | "avatar">;
// { id: string; name: string; email: string; bio: string; avatar: string }
깊은 변환 유틸리티
typescript
// 깊은 Readonly
type DeepReadonly<T> =
  T extends (...args: any[]) => any ? T :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T;
 
// 깊은 Mutable (readonly 제거)
type DeepMutable<T> =
  T extends (...args: any[]) => any ? T :
  T extends object ? { -readonly [K in keyof T]: DeepMutable<T[K]> } :
  T;
 
// 깊은 NonNullable
type DeepNonNullable<T> =
  T extends (...args: any[]) => any ? T :
  T extends object ? { [K in keyof T]-?: DeepNonNullable<NonNullable<T[K]>> } :
  NonNullable<T>;

타입 가드 유틸리티

타입 가드 헬퍼
typescript
// 특정 프로퍼티의 존재로 타입 좁히기
type HasProperty<T, K extends string> =
  T extends Record<K, unknown> ? T : never;
 
function hasProperty<T, K extends string>(
  obj: T,
  key: K
): obj is T & Record<K, unknown> {
  return typeof obj === "object" && obj !== null && key in obj;
}
 
// 사용
function processEvent(event: unknown) {
  if (hasProperty(event, "type")) {
    console.log(event.type);  // OK — event: unknown & Record<"type", unknown>
  }
}

NoInfer와 라이브러리 설계 원칙

NoInfer는 라이브러리 설계에서 추론의 주도권을 제어하는 도구입니다. 핵심 원칙은 다음과 같습니다.

원칙 1: 추론 소스를 하나로 통일

추론 소스 통일
typescript
// 나쁜 예: 여러 곳에서 T를 추론
function bad<T>(items: T[], defaultItem: T): T[] { /* ... */ }
 
// 좋은 예: items에서만 T 추론, defaultItem은 검증만
function good<T>(items: T[], defaultItem: NoInfer<T>): T[] { /* ... */ }

원칙 2: 입력에서 추론, 출력/옵션에서 검증

입력-추론, 옵션-검증 패턴
typescript
function createStore<State extends Record<string, unknown>>(config: {
  // State는 여기서 추론
  initialState: State;
  // 나머지는 State를 기반으로 검증
  computed?: {
    [key: string]: (state: NoInfer<State>) => unknown;
  };
  actions?: {
    [key: string]: (state: NoInfer<State>, payload: any) => NoInfer<State>;
  };
}) {
  // ...
}

원칙 3: 콜백의 매개변수는 추론을 차단

콜백 매개변수 추론 차단
typescript
function transform<T, R>(
  data: T[],
  fn: (item: NoInfer<T>) => R
): R[] {
  return data.map(fn);
}
 
// T는 data에서 추론, fn의 item은 검증만
const result = transform(
  [{ name: "Alice", age: 30 }],
  (item) => item.name  // item: { name: string; age: number }
);
Tip

모든 곳에 NoInfer를 사용할 필요는 없습니다. TypeScript의 기본 추론이 올바르게 동작하는 경우에는 NoInfer가 불필요합니다. 추론 결과가 의도와 다를 때, 또는 "이 위치는 추론에 참여하면 안 된다"는 의도를 명시하고 싶을 때만 사용하세요.

정리

NoInfer는 TypeScript 5.4에서 도입된 작지만 강력한 유틸리티 타입입니다. 타입 추론의 방향을 제어하여, "어디서 추론하고 어디서 검증할지"를 명시적으로 지정할 수 있습니다. 내장 유틸리티 타입과 커스텀 유틸리티 타입을 적절히 활용하면, 타입 안전하면서도 사용하기 편리한 API를 설계할 수 있습니다.

다음 장에서는 지금까지 배운 조건부 타입, 매핑 타입, 템플릿 리터럴 타입, infer, NoInfer를 결합하여 타입 수준 프로그래밍의 세계를 탐험합니다. 타입만으로 산술 연산, 문자열 파서, 상태 머신을 구현하는 고급 기법을 다룹니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#typescript#performance#devtools

관련 글

프로그래밍

10장: 타입 수준 프로그래밍 — 타입으로 로직 작성하기

TypeScript 타입 시스템을 프로그래밍 언어로 활용하는 고급 기법 — 산술 연산, 문자열 파서, 상태 머신 등을 타입만으로 구현하는 패턴을 다룹니다.

2026년 2월 9일·14분
프로그래밍

8장: infer 키워드와 타입 추론 마스터

TypeScript infer 키워드의 고급 활용 패턴, infer extends 구문, 공변/반변 위치에서의 추론, 그리고 실전 타입 추출 패턴을 심층 분석합니다.

2026년 2월 5일·14분
프로그래밍

11장: 프로젝트 설정과 모노레포 타입 전략

TypeScript 프로젝트의 tsconfig.json 최적화, 프로젝트 참조, Isolated Declarations, 모노레포에서의 타입 전략을 실전 중심으로 다룹니다.

2026년 2월 11일·14분
이전 글8장: infer 키워드와 타입 추론 마스터
다음 글10장: 타입 수준 프로그래밍 — 타입으로 로직 작성하기

댓글

목차

약 14분 남음
  • NoInfer의 등장 배경
  • NoInfer 유틸리티 타입
    • NoInfer의 동작 원리
  • 실전 패턴
    • 기본값 제한
    • 이벤트 시스템에서 페이로드 검증
    • 테마 시스템
    • 라우터에서 파라미터 제한
  • 5.x의 다른 유틸리티 타입 개선
    • 내장 유틸리티 타입 총정리
    • 커스텀 유틸리티 타입 설계
    • 타입 가드 유틸리티
  • NoInfer와 라이브러리 설계 원칙
    • 원칙 1: 추론 소스를 하나로 통일
    • 원칙 2: 입력에서 추론, 출력/옵션에서 검증
    • 원칙 3: 콜백의 매개변수는 추론을 차단
  • 정리