TypeScript 型別收窄 (Type Narrowing)
型別收窄 (Type Narrowing) 是 TypeScript 根據程式碼的控制流程,自動將較寬的型別縮小為更精確型別的過程。這讓我們可以安全地存取特定型別的屬性和方法。
typeof 型別守衛
使用 typeof 運算子可以收窄原始型別:
function printValue(value: string | number) {
if (typeof value === 'string') {
// value 被收窄為 string
console.log(value.toUpperCase());
} else {
// value 被收窄為 number
console.log(value.toFixed(2));
}
}
printValue('hello'); // HELLO
printValue(3.14159); // 3.14
typeof 可以檢查的型別:
"string""number""boolean""symbol""undefined""object"(注意:null也會回傳"object")"function""bigint"
真值收窄 (Truthiness Narrowing)
利用 JavaScript 的真值檢查來收窄型別:
function printName(name: string | null | undefined) {
if (name) {
// name 被收窄為 string
console.log(name.toUpperCase());
} else {
console.log('沒有名字');
}
}
// 使用邏輯運算子
function getLength(value: string | null): number {
return value?.length ?? 0;
}
假值 (falsy values) 包括:
false、0、""、null、undefined、NaN。空字串 "" 也是假值,所以用真值檢查時要注意。相等收窄 (Equality Narrowing)
使用 ===、!==、==、!= 進行比較:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// x 和 y 都被收窄為 string(唯一共同的型別)
console.log(x.toUpperCase());
console.log(y.toUpperCase());
}
}
// 檢查 null
function process(value: string | null) {
if (value !== null) {
// value 被收窄為 string
console.log(value.length);
}
}
in 運算子收窄
使用 in 運算子檢查物件是否有某個屬性:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
// animal 被收窄為 Bird
animal.fly();
} else {
// animal 被收窄為 Fish
animal.swim();
}
}
instanceof 運算子收窄
使用 instanceof 檢查建構函式:
function logDate(date: Date | string) {
if (date instanceof Date) {
// date 被收窄為 Date
console.log(date.toISOString());
} else {
// date 被收窄為 string
console.log(new Date(date).toISOString());
}
}
// 自訂類別也可以
class Cat {
meow() {
console.log('喵~');
}
}
class Dog {
bark() {
console.log('汪!');
}
}
function makeSound(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.bark();
}
}
指派收窄 (Assignment Narrowing)
TypeScript 會根據指派的值來收窄型別:
let value: string | number;
value = 'hello';
// 這裡 value 的型別是 string
console.log(value.toUpperCase());
value = 42;
// 這裡 value 的型別是 number
console.log(value.toFixed(2));
控制流程分析
TypeScript 會追蹤程式的控制流程:
function example(value: string | number | null) {
if (value === null) {
// value 是 null
return;
}
// value 是 string | number(null 被排除了)
if (typeof value === 'string') {
// value 是 string
console.log(value.length);
return;
}
// value 是 number(string 被排除了)
console.log(value * 2);
}
死碼分析
function example(value: string | number) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
return;
}
// 這裡 value 必定是 number
console.log(value.toFixed(2));
// 以下程式碼永遠不會執行
// TypeScript 知道這一點
}
可辨識聯合 (Discriminated Unions)
透過共同的字面量屬性來收窄型別:
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// shape 被收窄為 Circle
return Math.PI * shape.radius ** 2;
case 'rectangle':
// shape 被收窄為 Rectangle
return shape.width * shape.height;
case 'triangle':
// shape 被收窄為 Triangle
return (shape.base * shape.height) / 2;
}
}
never 型別與窮盡檢查
使用 never 確保處理了所有情況:
type Shape = Circle | Rectangle | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// 如果 Shape 新增了型別但這裡沒處理,會報錯
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
陣列收窄
function processArray(arr: string[] | number[]) {
// 無法直接存取,因為可能是 string[] 或 number[]
if (arr.length > 0) {
const first = arr[0];
if (typeof first === 'string') {
// arr 被收窄為 string[]
arr.forEach((item) => console.log(item.toUpperCase()));
} else {
// arr 被收窄為 number[]
arr.forEach((item) => console.log(item.toFixed(2)));
}
}
}
常見陷阱
typeof null
function example(value: object | null) {
// 錯誤的方式:typeof null === "object"
if (typeof value === 'object') {
// value 仍然可能是 null!
// value.toString(); // 可能報錯
}
// 正確的方式
if (value !== null) {
value.toString(); // OK
}
}
陣列與物件
function example(value: object | unknown[]) {
// typeof 對陣列和物件都回傳 "object"
if (typeof value === 'object') {
// 無法區分陣列和物件
}
// 使用 Array.isArray
if (Array.isArray(value)) {
// value 是 unknown[]
console.log(value.length);
}
}
實用範例
處理 API 回應
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string }
| { status: 'loading' };
function handleResponse<T>(response: ApiResponse<T>): T | null {
switch (response.status) {
case 'success':
return response.data;
case 'error':
console.error(response.message);
return null;
case 'loading':
console.log('載入中...');
return null;
}
}
表單值處理
type FormValue = string | number | boolean | null;
function formatFormValue(value: FormValue): string {
if (value === null) {
return '';
}
if (typeof value === 'boolean') {
return value ? '是' : '否';
}
if (typeof value === 'number') {
return value.toString();
}
// value 是 string
return value;
}
事件處理
function handleEvent(event: MouseEvent | KeyboardEvent) {
if (event instanceof MouseEvent) {
console.log(`滑鼠點擊位置: (${event.clientX}, ${event.clientY})`);
} else {
console.log(`按鍵: ${event.key}`);
}
}