TypeScript 타입 좁히기: union null unknown 안전 검증

··설명 약 7분·작성: 해비·블로그 소개

TypeScript union, null, unknown의 타입을 어떻게 안전하게 좁히나요?

여러 타입이 섞인 값은 필요한 조건을 확인한 분기 안에서 사용합니다. null을 먼저 나누고 unknown은 객체와 속성, 그 속성의 타입까지 검사하면 any 없이도 안전하게 읽을 수 있습니다.

적용 조건·범위: 객체 property를 callback 안에서 사용할 때 TypeScript가 변경 가능성을 고려해 narrowing을 보수적으로 처리하는 경우가 있습니다.

공식 문서 참고: TypeScript 핸드북: 타입 좁히기·판별 유니온·never

직접 확인: 오류에서 수정까지

TypeScript 타입 좁히기: union null unknown 안전 검증 · 독립 실습

얻을 결과: null 검사 뒤 문자열 ID로 작업하고 빈 선택 상태를 별도로 처리합니다.

예제 크게 보기 (새 탭)

단독 검사는 tsconfig.json이 없는 새 폴더에서 example.ts 한 파일씩 교체하며 진행합니다. TypeScript가 없다면 설치 글에서 Node.js/npm·TypeScript 준비를 확인하세요. 타입 검사 성공은 코드 실행 성공과 구분합니다.

독자 질문: 선택된 taskId가 null일 때 어떻게 안전하게 다룰까요?

얻을 결과: null 검사 뒤 문자열 ID로 작업하고 빈 선택 상태를 별도로 처리합니다.

오류 코드를 별도 example.ts에 복사합니다.

function label(id: string | null) { return id.toUpperCase(); }

예상 타입 오류 (TS18047): ‘id’ is possibly ‘null’입니다. null 분기 후에만 문자열 메서드를 호출합니다.

위 코드를 다음 수정 예제로 교체합니다.

function label(id: string | null): string {
  if (id === null) return '선택 없음';
  return id.toUpperCase();
}
console.log(label(null));
npx tsc --strict --noEmit example.ts

기존 최소 예제 확인 기록: TypeScript 7.0.2의 별도 최소 예제에서 오류 TS18047, 수정 후 exit 0 확인. TaskFlow 전체 체크포인트·브라우저 실행은 미확인.

다음 학습: 07회차에서 keyof·typeof로 기존 객체의 key 타입을 추출합니다.

union

아래 개념 조각에서 참조하는 학습용 타입입니다. 프로젝트 파일을 import하지 않습니다. 같은 이름을 다시 선언하는 비교 코드는 별도 파일에서 확인하세요.

type Task = { id: string; title: string; completed: boolean };
type TaskId =
  string | number;

TaskId는 string 또는 number입니다.

바로 string 전용 메서드를 사용할 수 없습니다.

function printId(
  id: TaskId,
) {
  if (
    typeof id === 'string'
  ) {
    console.log(
      id.toUpperCase(),
    );
  } else {
    console.log(
      id.toFixed(0),
    );
  }
}

조건문 이후 각 branch에서 타입이 좁혀집니다.

null 처리

function getTitle(
  task: Task | null,
): string {
  if (!task) {
    return '선택 없음';
  }


  return task.title;
}

strictNullChecks가 적용되면 null 가능성을 무시할 수 없습니다.

unknown

외부 입력을 any로 받으면 모든 property 접근이 허용됩니다.

function read(
  value: any,
) {
  return value.title;
}

unknown은 바로 property를 읽을 수 없습니다.

function read(
  value: unknown,
) {
  if (
    typeof value ===
      'object' &&
    value !== null &&
    'title' in value
  ) {
    return value.title;
  }
}

검증을 강제한다는 점이 중요합니다.

Array.isArray

function getLength(
  value: unknown,
): number {
  if (
    Array.isArray(value)
  ) {
    return value.length;
  }


  return 0;
}

배열 여부를 runtime에서 확인하면서 TypeScript 타입도 좁아집니다.

in operator

type Success = {
  data: string[];
};


type Failure = {
  error: string;
};


function print(
  result:
    Success | Failure,
) {
  if ('data' in result) {
    console.log(
      result.data,
    );
  } else {
    console.log(
      result.error,
    );
  }
}

property 존재 여부로 union을 좁힐 수 있습니다.

사용자 정의 type guard

function isTask(
  value: unknown,
): value is Task {
  if (
    typeof value !== 'object' ||
    value === null
  ) {
    return false;
  }


  return (
    'id' in value &&
    'title' in value &&
    'completed' in value
  );
}

이 함수가 true를 반환하면 TypeScript는 value를 Task로 취급합니다.

실제 runtime 검증은 property 타입까지 확인해야 더 안전합니다. 16회차에서 완성합니다.

narrowing이 풀리는 상황

객체 property를 callback 안에서 사용할 때 TypeScript가 변경 가능성을 고려해 narrowing을 보수적으로 처리하는 경우가 있습니다.

이럴 때 검증된 값을 지역 변수에 저장하는 방법이 도움이 될 수 있습니다.

const selected =
  task;


if (!selected) {
  return;
}


items.map(() =>
  selected.title,
);

자주 하는 실수

non-null assertion으로 건너뜁니다

task!.title

실제로 null이 될 수 있다면 runtime 오류를 숨기는 방식입니다.

unknown을 바로 as Task로 단언합니다

const task =
  value as Task;

검증한 것이 아닙니다.

학습 완료 체크리스트

null 분기를 없애면 왜 문자열 메서드가 거부되는지 설명하고, title 속성이 있는 객체라도 그 값이 숫자라면 문자열로 읽을 수 없는 이유를 구분해 보세요.


작은 변형으로 확인하기

label(null)을 label('abc')로 바꾸면 반환값은 ABC입니다. null 분기를 지우면 다시 타입 오류가 나는 이유를 설명하세요.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript 코드를 TypeScript로 확장합니다.

TaskFlow의 선택 task와 외부 입력을 안전하게 좁힙니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

TypeScript 타입 좁히기: union null unknown 안전 검증 · 단계별 TaskFlow

타입을 적용한 TaskFlow에서 작업을 추가·완료 전환·삭제하세요. 타입만 바뀐 단계는 이전과 화면이 같습니다.

예제 크게 보기 (새 탭)

변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.

시작본과 이번 변경

TypeScript type vs interface 객체 타입 설계 기준의 완료 체크포인트를 기준으로 이어갑니다. 변경하지 않는 파일은 그 시작 상태를 유지합니다.

이번 완료본의 src/core/selection.ts에 선택 함수를 추가해 null을 먼저 처리하고, 같은 파일의 readTitle은 unknown의 객체 여부와 title의 string 여부를 확인합니다. null·제목 없는 객체·정상 객체를 각각 대조합니다.

이전 체크포인트

typescript-step-05-object-types

이번 목표

TaskFlow의 선택 task와 외부 입력을 안전하게 좁힙니다.

선택 task

export function getSelectedTask(
  tasks: Task[],
  selectedId:
    string | null,
): Task | null {
  if (!selectedId) {
    return null;
  }


  return (
    tasks.find(
      (task) =>
        task.id ===
        selectedId,
    ) ?? null
  );
}

사용:

const selected =
  getSelectedTask(
    tasks,
    selectedId,
  );


if (selected) {
  console.log(
    selected.title,
  );
}

외부 값

export function readTitle(
  value: unknown,
): string | null {
  if (
    typeof value !== 'object' ||
    value === null ||
    !('title' in value) ||
    typeof value.title !==
      'string'
  ) {
    return null;
  }


  return value.title;
}

검증

npx tsc --noEmit

중간부터 시작하는 전체 파일과 적용 순서

이번 글 시작 ZIP · 이번 글 완료 ZIP

시작 ZIP은 직전 프로젝트 단계 typescript-step-05-object-types의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.

  1. 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
  2. 선택 함수와 외부 제목 읽기 함수를 core/selection.ts에 추가합니다. 사용 전 null을 확인합니다.
  3. 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
  4. 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.

추가·교체 파일

  • CHANGES.md — 전체 교체
  • README.md — 전체 교체
  • tests/contracts.ts — 전체 교체
  • src/core/selection.ts — 추가

실행과 확인

npm ci
npm run dev
npm run typecheck
npm test
npm run build

개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.

예상 결과: 선택 없음·없는 ID·null·숫자 제목은 null, 문자열 제목은 해당 문자열입니다.

기존 ZIP 검증 기록: TypeScript strict 검사·긍정/부정 타입 계약·런타임 검사·Vite 빌드 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.

이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.

복사할 변경 파일 전체 코드

tests/contracts.ts — 전체 파일

import type { Task } from '../src/types/task.js';
const valid: Task = {id:'1',title:'공부',completed:false};
// @ts-expect-error completed must be boolean
const invalid: Task = {id:'1',title:'공부',completed:'no'};
void valid; void invalid;
import { readTitle } from '../src/core/selection.js';
const nullable = readTitle(null);
// @ts-expect-error null result must be narrowed
const definite: string = nullable;
void definite;

src/core/selection.ts — 전체 파일

import type { Task } from '../types/task.js';
export function getSelectedTask(
  tasks: Task[],
  selectedId:
    string | null,
): Task | null {
  if (!selectedId) {
    return null;
  }


  return (
    tasks.find(
      (task) =>
        task.id ===
        selectedId,
    ) ?? null
  );
}
export function readTitle(
  value: unknown,
): string | null {
  if (
    typeof value !== 'object' ||
    value === null ||
    !('title' in value) ||
    typeof value.title !==
      'string'
  ) {
    return null;
  }


  return value.title;
}

완료 체크포인트

typescript-step-06-narrowing

다음 학습

다음 글에서는 객체 key와 실제 값에서 타입을 추출하는 keyof, typeof, indexed access type을 배웁니다.

다음 글: TypeScript keyof typeof 인덱스 접근 타입 사용법

글 소개·학습 준비·전체 목차
TypeScript 타입 좁히기: union null unknown 안전 검증

TypeScript · 6 / 22 · 필수 학습 · narrowing

union·null·unknown에서 안전하게 속성을 읽으려면? 사용하려는 연산에 필요한 조건을 먼저 확인하고 그 분기 안에서 좁혀진 타입을 사용합니다. null은 별도 분기하고 unknown은 객체·속성·속성 타입을 검사합니다. ID를 대문자로 바꾸는 단독 예제로 검사 전후 차이를 확인합니다.

이 과정을 narrowing이라고 합니다.

narrowing을 이해하면 any 없이도 null, 외부 입력, 여러 타입이 섞인 값을 안전하게 다룰 수 있습니다.

이번 글에서 배우는 내용

  • union
  • null과 undefined
  • typeof narrowing
  • in operator
  • Array.isArray
  • unknown
  • 사용자 정의 type guard

단독 학습 선수 지식

union·객체와 null/undefined, strictNullChecks을 알면 됩니다. 이전 TaskFlow 파일 없이 직접 확인 예제로 학습할 수 있습니다.

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 22개 · 필수 21개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기