如何在TypeScript层级数据类型上应用开闭原则(不可修改自动生成类型)
函数式实现符合开闭原则的Problem类型验证方案
现有不可修改的TypeScript类型
type BaseProblem = { name?: string; description?: string; } type SimProblem = BaseProblem & { type: "sim"; simNumber?: string; } type CarProblem = BaseProblem & { type: "car"; model?: string; plate: string; } type AllKeys<T> = T extends unknown ? keyof T : never; type Id<T> = T extends infer U ? {[K in keyof U]: U[K]} : never; type _ExclusifyUnion<T, K extends PropertyKey> = T extends unknown ? Id<T & Partial<Record<Exclude<K, keyof T>, never>>> : never; declare type ExclusifyUnion<T> = _ExclusifyUnion<T, AllKeys<T>>; type Problem = ExclusifyUnion<CarProblem | SimProblem>;
问题现状
当前的isValidToBeSent函数通过switch判断type字段实现验证,每次新增Problem类型或修改某类型的验证规则时,都要修改这个函数,完全违反开闭原则(对扩展开放,对修改关闭)。
函数式解决方案
核心思路是用策略模式的函数式实现:把每种类型的验证逻辑拆成独立函数,用注册表维护类型与验证函数的映射,新增类型只需要添加新的验证函数并注册,无需修改原有核心代码。
1. 定义统一的验证函数类型
先给所有验证函数定好类型约束,确保类型安全:
// 验证函数统一接口:接收对应类型的Problem,返回布尔值表示是否合法 type ProblemValidator<T extends Problem> = (problem: T) => boolean;
2. 实现各类型的独立验证逻辑
把原来switch里的逻辑拆成单独的函数,各自负责对应类型的验证规则:
// SimProblem专属验证逻辑 const validateSimProblem: ProblemValidator<SimProblem> = (problem) => { // 示例规则:simNumber不能为空 return !!problem.simNumber; }; // CarProblem专属验证逻辑 const validateCarProblem: ProblemValidator<CarProblem> = (problem) => { // 示例规则:plate必填,若填写model则不能为空 return !!problem.plate && (problem.model ? !!problem.model : true); };
3. 维护验证函数注册表
用对象映射type字段到对应验证函数,相当于一个可扩展的策略库:
// 注册表:key为Problem的type值,value为对应类型的验证函数 const problemValidators: Record<Problem["type"], ProblemValidator<any>> = { sim: validateSimProblem, car: validateCarProblem, };
4. 实现通用的验证入口函数
这个函数是固定的,后续扩展类型无需修改:
const isValidToBeSent = (problem: Problem): boolean => { // 根据type从注册表获取对应验证函数 const validator = problemValidators[problem.type]; // 兜底逻辑:找不到对应验证函数时默认返回不合法(可根据业务调整) if (!validator) return false; return validator(problem); };
5. 扩展新类型的方式
如果新增PhoneProblem类型,只需要两步操作:
// 假设新增自动生成的PhoneProblem类型 type PhoneProblem = BaseProblem & { type: "phone"; phoneNumber: string; }; // 更新Problem类型(同步自动生成的定义) type Problem = ExclusifyUnion<CarProblem | SimProblem | PhoneProblem>; // 1. 实现新类型的验证函数 const validatePhoneProblem: ProblemValidator<PhoneProblem> = (problem) => { // 示例规则:手机号格式校验 return /^1[3-9]\d{9}$/.test(problem.phoneNumber); }; // 2. 将验证函数注册到注册表 problemValidators.phone = validatePhoneProblem;
6. React组件中的使用示例
直接调用通用的isValidToBeSent即可,无需关心具体类型:
import { useState } from 'react'; const ProblemForm = () => { const [problem, setProblem] = useState<Problem>({ type: "car", plate: "" }); const handleSubmit = () => { if (isValidToBeSent(problem)) { console.log("提交合法问题:", problem); // 执行提交接口逻辑 } else { alert("问题信息不合法,请检查"); } }; // 省略表单输入、类型切换等逻辑... return ( <div className="problem-form"> {/* 表单输入组件 */} <button onClick={handleSubmit}>提交问题</button> </div> ); }; export default ProblemForm;
内容的提问来源于stack exchange,提问作者Bertuz
相关产品推荐
相关产品推荐

