- Published on
Union Type Narrowing
- Authors
- Name
in
與 is
實作 TypeScript 聯合型別縮窄
用 在 TypeScript 裡,"role" in person
這段語法其實是利用 JavaScript 的 in 運算子來做「屬性存在性檢查」,同時被 TypeScript 當作一種 type guard(型別守衛),用來把聯合型別(union)縮窄(narrow)到其中包含 role 屬性的那個分支。
// 範例
type Person = {
name: string;
role?: string;
}
function checkRole(person: Person) {
if ("role" in person) {
// TypeScript 知道 person 一定有 role 屬性
console.log(person.role);
}
}
回傳值可以用 person is Admin 或 person is User 來表示,這樣 TypeScript 就知道回傳值是 Admin 或 User 型別。
// 範例
export function isAdmin(person: Person): person is Admin {
return person.type === 'admin';
}
export function isUser(person: Person): person is User {
return person.type === 'user';
}
Partial<Omit<User, 'type'>>
動態篩選 User 陣列
filterUsers 函式解析:如何用 Omit
是移除屬性Partial
是把所有屬性變成可選
export function filterUsers(
persons: Person[],
criteria: Partial<Omit<User, 'type'>>
): User[] {
return persons
.filter(isUser) // ① 先篩選出所有 User
.filter((user) => { // ② 再對每個 user 進一步比對 criteria
// Object.keys 回傳 string[],這裡做型別斷言
const criteriaKeys = Object.keys(criteria)
as (keyof Omit<User, 'type'>)[];
// ③ 只要 criteria 裡每個欄位,都在 user 上「完全相符」
return criteriaKeys.every((fieldName) => {
return user[fieldName] === criteria[fieldName];
});
});
}
步驟說明
① persons.filter(isUser):利用事先寫好的 type-guard isUser(檢查 type==='user'),只保留 User。
② criteria: Partial<Omit<User,'type'>>
:傳入想篩選的欄位(name、age、occupation 任一或多個),且全部可選。
③ Object.keys(criteria):拿到使用者實際傳入的欄位陣列,透過 every 逐一檢查 user[fieldName] === criteria[fieldName],都相符才算符合篩選。
練習題
interface User {
type: 'user';
name: string;
age: number;
occupation: string;
}
interface Admin {
type: 'admin';
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{ type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
{ type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
{ type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
{ type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' },
{ type: 'user', name: 'Wilson', age: 23, occupation: 'Ball' },
{ type: 'admin', name: 'Agent Smith', age: 23, role: 'Anti-virus engineer' }
];
export function logPerson(person: Person) {
console.log(
` - ${person.name}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
);
}
export function filterPersons<
T extends Person['type']
>(
persons: Person[],
personType: T,
criteria: Partial< Omit< Extract<Person, { type: T }>, 'type' >>
): Array< Extract<Person, { type: T }> > {
return persons
// 先用 type guard 確保輸入陣列符合 T
.filter((p): p is Extract<Person, { type: T }> => p.type === personType)
// 再根據 criteria 裡的 key/value 做過濾
.filter((person) => {
const keys = Object.keys(criteria) as Array< keyof Omit<Extract<Person, { type: T }>, 'type'> >
return keys.every((key) =>
person[key] === criteria[key]
)
})
}
export const usersOfAge23 = filterPersons(persons, 'user', { age: 23 });
export const adminsOfAge23 = filterPersons(persons, 'admin', { age: 23 });
console.log('Users of age 23:');
usersOfAge23.forEach(logPerson);
console.log();
console.log('Admins of age 23:');
adminsOfAge23.forEach(logPerson);
這樣寫也可把紅字修掉,但練習網站上的測試沒過
export function filterPersons(
persons: Person[],
personType: Person['type'],
criteria: Partial<Omit<Person, 'type'>>
): Person[] {
return persons
.filter((p): p is Extract<Person, { type: typeof personType }> => p.type === personType)
.filter((person) => {
const keys = Object.keys(criteria) as Array<keyof typeof person>;
return keys.every((key) => person[key] === (criteria as any)[key]);
});
}