목차
- 목차
- 1. Narrowing 기본 개념
- 2.
typeof& 타입 가드 - 3. Truthiness Narrowing (Truthy/Falsy 좁히기)
- 4. Equality Narrowing (동등성 좁히기)
- 5.
in연산자 Narrowing - 6.
instanceofNarrowing - 7. Assignments Narrowing (할당을 통한 타입 좁히기)
- 8. Control Flow Analysis (제어 흐름 분석)
- 9. Type Predicates (사용자 정의 타입 가드)
- 10. Discriminated Unions (식별 가능한 유니언 타입)
- 11.
never& Exhaustiveness Checking (never타입 & 빠짐없이 검사하기) - 12. 보너스 (실전 설계 문제)
이 글은 타입스크립트 핸드북 Narrowing를 읽고 스터디 발표를 위해 정리한 글입니다.
스터디원 모두가 해당 내용을 이미 읽었기 때문에, 학습 효과를 높이고자 퀴즈 형식으로 재구성하여 정리했습니다. 감사합니다.
1. Narrowing 기본 개념
Q1. Narrowing은 if/else, 삼항, return 같은 제어 흐름을 따라가며 타입을 더 구체적으로 만든다.
→ O / X 이유도 함께 말씀해주세요
- 정답 ⇒ O 타입스크립트는 제어 흐름 분석(Control Flow Analysis)으로 실행 경로를 추적하며 타입을 좁힌다.
2. typeof & 타입 가드
Q2. typeof v === "number" 블록 안에서 v의 타입은? 이유도 함께 말씀해주세요
function format(v: string | number) {
if (typeof v === "number") {
// 여기서 v의 타입은?
return v.toFixed(1);
}
return v.toUpperCase();
}
stringnumberstring | number
- 정답
⇒ 2번
typeof는 타입 가드로 동작하여 분기 내부에서number로 좁혀진다.
3. Truthiness Narrowing (Truthy/Falsy 좁히기)
Q3. if (name)이 거짓이 되는 값은? (다중 선택) 이유도 함께 말씀해주세요
function greet(name?: string) {
if (name) return `Hi, ${name}`;
return "Hi, stranger";
}
"Alice"""undefined"0"
- 정답
⇒ 2, 3번
""와undefined는 falsy."0"(문자열)은 truthy이다. ※ 빈 문자열 누락 위험을 항상 염두!
4. Equality Narrowing (동등성 좁히기)
Q4. if 블록 안에서 x, y의 타입은? 이유도 함께 말씀해주세요
function same(x: string | number, y: string | boolean) {
if (x === y) {
return [x, y]; // 여기서 x, y의 타입은?
}
}
stringnumberbooleanstring | number | boolean
- 정답
⇒ 1번
공통 가능 타입은
string뿐이라 두 값 모두string으로 좁혀진다.
Q4-보너스. value != null은 null과 undefined 둘 다를 걸러낸다.
→ O / X 이유도 함께 말씀해주세요
- 정답
⇒ O
느슨한 비교(부등 연산자)
!=는null | undefined동시 제거에 유용하다.
5. in 연산자 Narrowing
Q5. 각 분기에서 a의 타입으로 올바른 것은? 이유도 함께 말씀해주세요
type Cat = { meow: () => void };
type Dog = { bark: () => void };
type Human = { meow?: () => void; bark?: () => void };
function speak(a: Cat | Dog | Human) {
if ("meow" in a) {
// 여기서 a의 타입은?
return "maybe cat or human";
}
// 여기서 a의 타입은?
return "maybe dog or human";
}
- 참분기:
Cat | Human/ 거짓분기:Dog - 참분기:
Cat/ 거짓분기:Dog | Human - 참분기:
Cat | Human/ 거짓분기:Dog | Human - 참분기:
Cat/ 거짓분기:Dog
- 정답
⇒ 3번
Human의meow?는 선택적이라 있을 수도/없을 수도 있음 →"meow" in a가 참이면Cat | Human, 거짓이면Dog | Human가 남는다. (선택적 프로퍼티(?)가 있는 타입은 양쪽 분기에 모두 남을 수 있음)

6. instanceof Narrowing
Q6. if 블록 안에서 x의 타입은? 이유도 함께 말씀해주세요
function info(x: Date | URL) {
if (x instanceof URL) {
// 여기서 x의 타입은?
return x.hostname;
}
return x.toISOString();
}
DateURLDate | URL
- 정답
⇒ 2번
instanceof는 해당 생성자의 프로토타입 체인을 따라 타입을 좁혀준다.
7. Assignments Narrowing (할당을 통한 타입 좁히기)
Q7. 각 줄에서 관찰되는 타입으로 맞는 것은? 이유도 함께 말씀해주세요
let v: string | number = Math.random() > 0.5 ? "hi" : 7;
v = 9; // 지금 관찰되는 타입은?
v = "bye"; // 지금 관찰되는 타입은?
- 첫 재할당 후
number, 그 다음string - 둘 다
string | number - 둘 다
any
- 정답
⇒ 1번
해설은 보너스 문제와 함께
Q7-보너스. 마지막 v는 타입 에러가 발생할까? 이유도 함께 말씀해주세요
→ O / X
let v: string | number = Math.random() > 0.5 ? "hi" : 7;
v = 9;
v = "bye";
v = true; // 타입 에러가 발생할까?
- 정답
⇒ O
boolean이string | number타입에 포함되지 않으므로 오류가 발생한다.
해설: 현재 값에 의해 순간적으로 타입은 좁혀 보이지만, 결국 넣을 수 있는 값의 범위는 처음 선언된 타입(string | number)에 의해 결정된다.
8. Control Flow Analysis (제어 흐름 분석)
Q8. return으로 한 분기가 종료되면, 남은 경로에서는 해당 분기의 타입 가능성은 제거되어 더 좁혀진다.
이에 대한 올바른 설명은?
return은 타입과 관계없고 단순히 함수 실행만 끝낸다.return으로 분기가 끝나면, 이후 경로에서 해당 타입 가능성은 제거되어 타입이 좁혀진다.return은 분기와 상관없이 항상 타입을 그대로 유지한다.return이 나오면 타입은 좁혀지지만,strictNullChecks를 꺼야 동작한다.
- 정답
⇒ 2번
타입스크립트는 코드의 실행 흐름(도달 가능성)을 따라가면서, 조건문·할당마다 변수가 가질 수 있는 타입을 좁히고, 분기와 합류 지점에서 타입을 다시 계산한다.
return은 함수 실행을 끝낸다는 건 맞지만, 타입 분석에도 직접적인 영향을 준다.- 어떤 분기가
return으로 끝나면 그 경로는 더 이상 실행될 수 없음 따라서 타입스크립트는 그 분기에 해당하는 타입 가능성을 제거함
- 어떤 분기가
- 타입스크립트는 제어 흐름 분석(Control Flow Analysis) 을 수행한다.
- 따라서
if분기 안에서return을 만나면, 나머지 코드에서는 그 조건을 만족하는 타입이 제외됨 - 즉, 남은 코드에서 타입이 더 좁혀진 상태로 추론됨
- 따라서
- 분기와 상관없이 타입을 그대로 유지한다면, 타입스크립트는 타입 안전성을 제대로 보장하지 못한다.
strictNullChecks는null/undefined를 엄격하게 다루는 옵션일 뿐, 제어 흐름 분석 자체에는 영향을 주지 않음- 즉, return에 따른 타입 좁히기는
strictNullChecks와 상관없이 항상 동작함
- 즉, return에 따른 타입 좁히기는
9. Type Predicates (사용자 정의 타입 가드)
Q9. 아래 예제에서 isOk(something)를 반환한 분기 안에서 something의 타입은?
이유도 함께 말씀해주세요
function isOk(v: unknown): v is { ok: true } {
return typeof v === "object" && v !== null && (v as any).ok === true;
}
const something: unknown = { ok: true };
if (isOk(something)) {
// 여기서 something 타입은 무엇일까요?
console.log(typeof something);
} else {
// 여기서 something 타입은 무엇일까요?
console.log(typeof something);
}
- 번호
- if 분기 안:
something은{ ok: true } - else 분기 안:
something은unknown
- if 분기 안:
- 번호
- if 분기 안:
something은object - else 분기 안:
something은null
- if 분기 안:
- 번호
- if 분기 안:
something은any - else 분기 안:
something은never
- if 분기 안:
- 정답
⇒ 1번
사용자 정의 타입 가드(
v is { ok: true }) 덕분에isOk(something)이true일 때는 타입이{ ok: true }로 좁혀지고,false일 때는 원래 타입인unknown으로 남는다.

10. Discriminated Unions (식별 가능한 유니언 타입)
Q10. 아래 login 함수가 각 분기에서 안전한 이유는 무엇일까요?
type Login =
| { type: "password"; id: string; pw: string }
| { type: "oauth"; provider: "google" | "github"; token: string };
function login(l: Login) {
switch (l.type) {
case "password":
return l.pw.length;
case "oauth":
return l.provider.toUpperCase();
}
}
switch에서 각 케이스에return이 있어서strictNullChecks옵션 설정 덕분에- 공통 리터럴 필드로 각 케이스의 필수 프로퍼티가 보장되니까
any타입으로 암묵 변환되기 때문에
- 정답
⇒ 3번
**식별 가능한 유니언 타입(Discriminated Union)**은
유니언 타입의 모든 멤버가 **공통 필드(discriminant)**를 가진 경우 동작함
그 공통 필드에 리터럴 타입 값(예제에선
type:필드)이 들어가면, 타입스크립트는if나switch조건 검사에 따라 자동으로 타입을 좁혀줌
11. never & Exhaustiveness Checking (never 타입 & 빠짐없이 검사하기)
Q11. default의 역할은?
type Animal = { kind: "cat"; meow: string } | { kind: "dog"; bark: string };
function speak(a: Animal) {
switch (a.kind) {
case "cat":
return a.meow;
case "dog":
return a.bark;
default:
const _ex: never = a;
return _ex;
}
}
- 아무 의미 없음
- 새 케이스 추가 시 누락을 컴파일 타임 에러로 잡아줌
- 런타임 최적화
- 정답
⇒ 2번
새로운 멤버가 추가되면, 아직 처리되지 않은 타입이 남기 때문에
a가never가 될 수 없어 에러로 경고한다
12. 보너스 (실전 설계 문제)
Q12. 다음 옵션 설계를 **Discriminated Union(식별 가능한 유니언 타입)**으로 안전하게 바꿔주세요
interface Pay {
method: "card" | "cash";
cardNo?: string;
amount?: number;
}
// method가 "card"인데 cardNo가 없어도 통과됨 → 타입에서 걸러지지 않음
const card: Pay = { method: "card" };
- 정답
-
정답
type Pay = | { method: "card"; cardNo: string } | { method: "cash"; amount: number };
- 정답
interface CardPay {
method: "card";
cardNo: string;
amount: number;
}
interface CashPay {
method: "cash";
amount: number;
}
type Pay = CardPay | CashPay;