概述
- 在TypeScript中,
extends
關鍵字是類型系統中一個極其重要的組成部分 - 它不僅用于類的繼承,也是類型兼容性檢查和泛型約束的關鍵機制
- 特別是當它與
keyof
關鍵字結合,形成K extends keyof T
的結構時 - 它為類型系統帶來了強大的靈活性和表達能力,讓我們能夠在泛型中對對象的屬性進行操作和約束
K extends keyof T
- 在TypeScript中,當你聲明一個泛型約束為
K extends keyof T
時 - 這意味著泛型參數K被限制為只能是T類型上存在的屬性名的子集
- 這在處理對象屬性、映射類型或者條件類型時非常有用
示例1
interface User {id: number;name: string;email: string;
}function getProperty<T, K extends keyof T>(user: T, key: K): T[K] {return user[key];
}const user = { id: 1, name: "Alice", email: "alice@example.com"};
console.log(getProperty(user, "name")); // 輸出 "Alice"
- 在這個例子中,
K extends keyof User
確保了getProperty函數的key參數, 只能是User接口中定義的屬性名
示例2 屬性全面轉化成只讀
interface User {id: number;name: string;email: string;
}type ReadonlyStringFields<T> = {readonly [P in keyof T]: T[P];}type ReadonlyUser = ReadonlyStringFields<User>;// ReadonlyUser 類型為:
// {
// id: number;
// readonly name: string;
// readonly email: string;
// }
- 這里,ReadonlyStringFields 將所有屬性轉化為只讀,當然
示例3:部分屬性只讀
type MakeSomePropertiesReadonly<T, K extends keyof T> = {readonly [P in K]: T[P];
} & {[P in Exclude<keyof T, K>]: T[P];
};interface UserInfo {id: number;username: string;email: string;isAdmin: boolean;
}type ReadonlyUserDetails = MakeSomePropertiesReadonly<UserInfo, 'id' | 'email'>;function displayUserInfo(user: ReadonlyUserDetails) {console.log(`ID: ${user.id}, Email: ${user.email}`);// 下面這行如果嘗試在真實代碼中執行,會因為類型檢查而在編譯時失敗// user.id = 123; // Error: Cannot assign to 'id' because it is a read-only property.user.username = "NewUsername"; // 這是允許的,因為 username 不是只讀的
}// 假設我們有一個UserInfo實例,為了演示,直接構造一個符合 ReadonlyUserDetails 的對象
const userDetails: ReadonlyUserDetails = {id: 42,username: "JohnDoe",email: "john.doe@example.com",isAdmin: false,
};displayUserInfo(userDetails);
在 MakeSomePropertiesReadonly<T, K>
中- 泛型參數:
T
: 表示你想要修改屬性可讀性的原始對象類型K extends keyof T
: 表示一個泛型約束,要求 K 必須是 T 類型的鍵(即屬性名)的一個子集。這意味著你可以指定 T 中任意數量和名稱的屬性來變為只讀
- 類型別名結構:
{ readonly [P in K]: T[P]; }:
這部分創建了一個新類型,其中 K 集合內的每個屬性 P 被聲明為只讀。[P in K] 是一個映射類型,遍歷 K 中的所有鍵,并為每個鍵創建一個屬性,其值類型與 T[P] 相同,但加上了 readonly 修飾符。& { [P in Exclude<keyof T, K>]: T[P]; }
這部分用來保留 T 類型中未被指定為只讀的那些屬性。Exclude<keyof T, K>
是一個實用類型,用于從 T 的所有鍵中排除已經在 K 中的鍵,確保剩余的屬性不被重復定義且保持原樣。
K extends keyof any
- 當K extends keyof any時,這個約束實際上沒有起到任何限制作用
- 因為any類型在TypeScript中是最寬泛的類型,表示可以代表任何類型,所以任何類型都可以被認為是any的鍵
- 這通常在你想要泛型參數可以是任何類型時使用,但這種用法在實踐中較少見,因為失去了類型安全性的優勢
示例
function getProperty<K extends keyof any>(obj: any, key: K): any { return obj[key];
} const person = { name: 'Alice', age: 30, address: '123 Main Street'
}; // 由于使用了 keyof any,我們可以傳入任何類型的鍵
const name = getProperty(person, 'name'); // string
const age = getProperty(person, 'age'); // number
const address = getProperty(person, 'address'); // string
const unknownProp = getProperty(person, 'unknownProp'); // undefined,但不會引發類型錯誤 console.log(name); // 輸出: Alice
console.log(age); // 輸出: 30
console.log(address); // 輸出: 123 Main Street
console.log(unknownProp); // 輸出: undefined
keyof any
表示任何可能的屬性名,因為any
類型可以包含任意屬性- 使用
K extends keyof any
實際上對K
沒有太多限制,它可以是任意字符串或符號 - 這種約束通常不是很有用,因為它不提供關于K具體可能是什么的明確信息
K extends keyof (string | number | symbol)
- 這個表達式意味著泛型參數K可以是string或symbol類型中任何一個的鍵名
- 在TypeScript中,對象的鍵通常是字符串或符號類型,但不包括數字(除非使用了計算屬性名)
- 因此,這個約束在直覺上可能用于處理特殊情況,比如當你知道泛型參數可能被用于索引一個映射到字符串或符號屬性上,但實際上在標準對象操作中,數字作為鍵的用法不常見
示例
type AcceptableKeys = string | number | symbol; function processKey<K extends AcceptableKeys>(key: K): void {console.log(`Processing key: ${key.toString()}`);
} // 使用字符串作為鍵
processKey("myStringKey");
// 使用數字作為鍵
processKey(123); // 使用符號作為鍵
const mySymbol = Symbol("mySymbol");
processKey(mySymbol);
- 在這個示例中,我們定義了一個類型別名
AcceptableKeys
,它表示可以接受的鍵類型是字符串、數字或符號 - 然后,我們定義了一個泛型函數
processKey
,它接受一個類型為 K 的參數,其中 K 被約束為必須擴展(extends)AcceptableKeys
- 這樣,我們就可以向
processKey
函數傳遞字符串、數字或符號類型的參數