好好好,能讓 DeepSeek 思考 266 秒,還得是你 TypeScript 類型體操 | 類型系統循環依賴
大晚上突發奇想,要鍛煉身體。
做個什么運動呢?要選個室內的、適合我這個小小出租屋的,最好還是有點樂趣和挑戰性的。
那就做個體操吧!對, TypeScript 類型體操!
咱第一次做這個體操,于是想了個“簡單點”的場景,練練手。
我的需求是這樣的:
- 有兩個接口, IEncryptionAlgorithm 和 IEncryptionAlgorithmConfig ,分別是加密算法的接口和配置接口。
- 還有一個 EncryptionService<T, U> , T 是加密算法的接口, U 是配置接口。
// 加密算法配置接口
export interface IEncryptionAlgorithmConfig { }
// 加密算法接口
export interface IEncryptionAlgorithm<T extends IEncryptionAlgorithmConfig> { }
// 加密服務
export class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<TConfig>,
TConfig extends IEncryptionAlgorithmConfig
> { }
這樣,用就可以自己定義加密算法和配置了。比如用戶可以實現一個 AESGCMEncryption 加密算法,和一個 AESGCMEncryptionConfigFromEnv 配置類。
export interface IAESGCMEncryptionConfig extends IEncryptionAlgorithmConfig {
password: string;
salt: Uint8Array;
iv: Uint8Array;
}
export class AESGCMEncryption implements IEncryptionAlgorithm<IAESGCMEncryptionConfig> { }
export class AESGCMEncryptionConfigFromEnv implements IAESGCMEncryptionConfig { }
但是問題來了,對于 AESGCMEncryption 加密算法,它的配置類必須是 IAESGCMEncryptionConfig ,否則就無法正確獲取 password 、 salt 、 iv 這三個屬性。
然而,在目前的類型聲明中,這一點是無法做到的:
// encryptionService 類型是正確的,沒問題
let encryptionService: EncryptionService<AESGCMEncryption, AESGCMEncryptionConfigFromEnv>;
// encryptionService2 也是正確的,類型系統沒有報錯
// 但是應該報錯,因為 IEncryptionAlgorithmConfig 沒有 password 、 salt 、 iv 這三個屬性
// IEncryptionAlgorithmConfig 并不適合 AESGCMEncryption 加密算法
let encryptionService2: EncryptionService<AESGCMEncryption, IEncryptionAlgorithmConfig>;
那么我當然是求助我的體操教練 deepseek-r1 。
教練說:這個問題好解決,但是你先去洗個澡再回來。
我說:老師我看您面色如鐵,怎么好像有點猶豫?
教練說:幾百年沒見過玩 TypeScript 類型體操的了!你也就是走運,今天我有心情理你!
第一次問答 DeepSeek 給出的解決方案:
// 新增類型工具:從算法類中提取配置類型
export type ConfigType<T> = T extends IEncryptionAlgorithm<infer C> ? C : never;
export class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<TConfig>,
TConfig extends IEncryptionAlgorithmConfig = ConfigType<TAlgorithm>
> { }
很遺憾,并沒有解決問題。
再次向教練描述情況后,教練終于給出了正確方案:
export type EncryptionConfigType<T> = T extends IEncryptionAlgorithm<infer C> ? C : never;
export class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<any>,
TConfig extends EncryptionConfigType<TAlgorithm> = EncryptionConfigType<TAlgorithm>
> { }
這次終于解決了問題。
最后,教練授之以漁。
DeepSeek-R1
以下是關于這個 TypeScript 類型問題的原理剖析,通過循序漸進的分析來理解背后的類型系統邏輯:
第一章:基礎場景分析
1.原始問題
我們有一個加密服務類 EncryptionService,它需要兩個泛型參數:
class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<TConfig>,
TConfig extends IEncryptionAlgorithmConfig
> {}
期望實現:
- 當 TAlgorithm 是 AESGCMEncryption 時
- TConfig 必須 是 IAESGCMEncryptionConfig
- 不能 使用其他類型(如基礎接口 IEncryptionAlgorithmConfig)
2.直觀嘗試
用戶最初嘗試這樣定義:
class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<TConfig>,
TConfig extends IEncryptionAlgorithmConfig = ConfigType<TAlgorithm>
> {}
期望:
- TConfig 自動從 TAlgorithm 中提取配置類型。
- 實現類型關聯約束。
但發現以下用法沒有報錯:
let service: EncryptionService<AESGCMEncryption, IEncryptionAlgorithmConfig>; // 不報錯 ?
第二章:類型系統的局限
1.循環依賴問題
當這樣定義時:
TAlgorithm extends IEncryptionAlgorithm<TConfig>
TConfig extends ConfigType<TAlgorithm>
實際上創建了循環依賴:
- 要確定 TAlgorithm 需要先知道 TConfig。
- 要確定 TConfig 又需要先知道 TAlgorithm。
TypeScript 無法解析這種循環依賴,會觸發錯誤:
Type parameter 'TConfig' has a circular constraint.ts(2313)
2.類型推導的妥協
當使用:
TConfig extends IEncryptionAlgorithmConfig = ConfigType<TAlgorithm>
實際上:
- TConfig 的約束是寬泛的 IEncryptionAlgorithmConfig
- 默認值使用 ConfigType<TAlgorithm> 推導
結果:
- 顯式指定 TConfig 時(如 IEncryptionAlgorithmConfig)。
- 只要滿足 IEncryptionAlgorithmConfig 的約束就能通過檢查。
- 失去了與 TAlgorithm 的嚴格關聯。
第三章:突破循環依賴
1.關鍵思路轉變
我們需要:
- 解除 TAlgorithm 和 TConfig 的直接循環。
- 通過第三方類型工具建立間接關聯。
2.引入類型提取工具
定義條件類型:
type EncryptionConfigType<T> =
T extends IEncryptionAlgorithm<infer C> ? C : never;
這個類型的作用:
- 如果 T 是實現了 IEncryptionAlgorithm<C> 的類。
- 則提取出配置類型 C。
3.重構泛型參數
class EncryptionService<
TAlgorithm extends IEncryptionAlgorithm<any>, // 臨時放寬約束
TConfig extends EncryptionConfigType<TAlgorithm> // 關鍵約束
> {}
4.分步解析
當用戶這樣使用時:
type Test1 = EncryptionConfigType<AESGCMEncryption>;
// 等價于 IAESGCMEncryptionConfig
type Test2 = EncryptionConfigType<string>; // 得到 never
此時泛型參數的邏輯:
- 先確定 TAlgorithm 是某個加密算法類
- 通過 EncryptionConfigType<TAlgorithm> 提取其配置類型
- 要求 TConfig 必須匹配該配置類型
第四章:類型推導過程
1.合法用法分析
let service: EncryptionService<AESGCMEncryption>;
推導過程:
- TAlgorithm = AESGCMEncryption
- TConfig = EncryptionConfigType<AESGCMEncryption> → IAESGCMEncryptionConfig
- 驗證 AESGCMEncryption 是否滿足 IEncryptionAlgorithm<any> → 滿足
2.非法用法分析
let service: EncryptionService<AESGCMEncryption, IEncryptionAlgorithmConfig>;
推導過程:
- TAlgorithm = AESGCMEncryption
- TConfig = IEncryptionAlgorithmConfig
- 檢查 IEncryptionAlgorithmConfig 是否滿足 EncryptionConfigType<AESGCMEncryption> → 等價于檢查 IEncryptionAlgorithmConfig extends IAESGCMEncryptionConfig → 失敗
第五章:關鍵差異對比
1.原始方案的問題
class Service<TAlgo extends IEncryptionAlgorithm<TConfig>, TConfig...>
建立了:
TAlgo → TConfig → TAlgo → TConfig...
形成閉環,類型系統無法展開推導
2.最終方案的優勢
class Service<TAlgo extends IEncryptionAlgorithm<any>, TConfig...>
建立:
TAlgo → (通過 EncryptionConfigType) → TConfig
形成單向推導鏈,打破循環
第六章:技術總結
1.核心知識點
條件類型(Conditional Types):
T extends U ? X : Y
允許根據類型關系進行類型推導
類型推斷(infer):
T extends IEncryptionAlgorithm<infer C> ? C : never
從泛型類型中提取嵌套類型
泛型約束解耦:通過中間類型工具打破循環依賴
2. 設計原則
關注點分離:
- 算法類的定義與其配置類型解耦
- 通過類型工具建立關聯
推導優于顯式聲明:
- 優先讓類型系統自動推導
- 顯式聲明時進行嚴格檢查
第七章:擴展思考
1.如果增加新加密算法
class RSAEncryption implements IEncryptionAlgorithm<IRSAConfig> {}
類型系統會自動保證:
let service: EncryptionService<RSAEncryption>; // TConfig = IRSAConfig
let service: EncryptionService<RSAEncryption, IAESGCMEncryptionConfig>; // 錯誤 ?
2.為什么使用 any 是安全的
在:
TAlgorithm extends IEncryptionAlgorithm<any>
中:
- any 僅用于臨時繞過類型檢查。
- 實際約束由 TConfig extends EncryptionConfigType<TAlgorithm> 保證。
- 最終使用時類型系統仍會驗證實際類型。
通過這種層層遞進的分析,我們可以理解 TypeScript 類型系統在處理復雜泛型關系時的行為模式,以及如何通過合理的類型工具設計來實現嚴格的類型關聯約束。