UNPKG

default-template

Version:

一个用于定义默认值和转换对象结构的实用工具

403 lines (387 loc) 11.8 kB
/** * 用于自定义键名转换的函数类型 * @param oldKey 原始键名 * @returns 转换后的新键名 */ export type TemplateNewKeyFn = (oldKey: string) => string; /** * 默认模板选项接口 */ export interface DefaultTemplateOption { /** * 用于匹配所有键的通配符键名,默认为 '?' */ allTemplateKey?: string; } /** * 表示一个普通对象的接口 */ export interface PlainObject { /** * 索引签名,允许任意字符串键和任意类型值 */ [key: string]: any; } /** * 模板重命名类,用于处理对象键名的重命名和值的转换 */ export class TemplateRename { /** * 新的键名或键名转换函数 */ public key: string | TemplateNewKeyFn; /** * 值的模板定义 */ public def: any; /** * 创建一个模板重命名实例 * @param key 新的键名或键名转换函数 * @param def 值的模板定义 */ constructor(key: string | TemplateNewKeyFn, def: any) { this.key = key; this.def = def; } } /** * 判断值是否为基本类型或特定的内置对象类型 * * 以下类型被视为基本类型: * - null * - undefined * - boolean * - number * - string * - function * - bigint * - symbol * - Date 实例 * - Set 实例 * - Map 实例 * * @param val 需要判断的值 * @returns 如果是基本类型或特定的内置对象类型则返回 true,否则返回 false */ function isBaseType(val: any): boolean { return ( val == null || typeof val === 'boolean' || typeof val === 'number' || typeof val === 'string' || typeof val === 'function' || typeof val === 'bigint' || typeof val === 'symbol' || val instanceof Date || val instanceof Set || val instanceof Map ); } /** * 判断值是否为普通对象(不包括数组和特定的内置对象类型) * * 以下条件必须同时满足: * - 不为 null * - typeof 结果为 'object' * - 不是数组 * - 不是 Date 实例 * - 不是 Set 实例 * - 不是 Map 实例 * * @param val 需要判断的值 * @returns 如果是普通对象则返回 true,否则返回 false */ function isObject(val: any): val is PlainObject { return val !== null && typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date) && !(val instanceof Set) && !(val instanceof Map); } /** * 将值转换为指定类型的实例 * * 支持以下类型的转换: * - boolean: 字符串 'false' 转为 false,其他值使用 Boolean() 转换 * - number: 尝试将字符串或数字转换为有效数字,失败则返回默认值 * - string: null/undefined 返回默认值,其他值使用 String() 转换 * - Date: 如果不是 Date 实例则返回默认值 * - Set: 如果不是 Set 实例则返回默认值 * - Map: 如果不是 Map 实例则返回默认值 * - symbol: 如果不是 symbol 则返回默认值 * * @param type 目标类型的示例值 * @param val 需要转换的值 * @returns 转换后的值 */ function convertToType(type: any, val: any) { if (typeof type === 'boolean') { if (typeof val === 'string' && val.toLowerCase() === 'false') return false; return Boolean(val); } if (typeof type === 'number') { if (typeof val === 'string' && val !== '') { const n = Number(val); if (Number.isFinite(n)) return n; } if (typeof val === 'number' && Number.isFinite(val)) return val; return type; } if (typeof type === 'string') { return val == null ? type : String(val); } if (type instanceof Date) { if (val instanceof Date) return val; return type; } if (type instanceof Set) { if (val instanceof Set) return val; return type; } if (type instanceof Map) { if (val instanceof Map) return val; return type; } if (typeof type === 'symbol') { if (typeof val === 'symbol') return val; return type; } return val; } /** * 根据模板类型 T 递归推导最终返回类型: * - 如果是函数,取其返回值类型 * - 如果是数组,递归处理元素类型 * - 如果是对象,递归处理每个字段 * - 其他类型原样返回 */ export type TemplateResult<T> = T extends (...args: any[]) => infer R ? R : T extends Array<infer U> ? Array<TemplateResult<U>> : T extends object ? { [K in keyof T]: TemplateResult<T[K]> } : T; /** * 默认值模板函数,用于处理数据结构的默认值、类型转换和键名映射 * * 主要功能: * 1. 为 null/undefined 值提供默认值 * 2. 自动转换值类型以匹配模板类型 * 3. 支持数组元素的模板处理 * 4. 支持对象属性的模板处理和键名映射 * 5. 支持通配符键处理所有未指定的属性 * * @example 基本类型处理 * ```typescript * defaultTemplate(10, undefined) // 返回: 10 * defaultTemplate(10, '20') // 返回: 20 (类型转换) * defaultTemplate(false, 'true') // 返回: true (类型转换) * ``` * * @example 数组处理 * ```typescript * defaultTemplate([10], [null, '20', 30]) // 返回: [10, 20, 30] * defaultTemplate([{ min: 0 }], [{ val: -1 }, { val: 5 }]) * // 返回: [{ min: 0, val: -1 }, { min: 0, val: 5 }] * ``` * * @example 对象处理 * ```typescript * defaultTemplate( * { a: 10, b: false, '?': true }, * { a: '20', c: null } * ) // 返回: { a: 20, c: true } * * // 嵌套对象处理 * defaultTemplate( * { user: { name: '', age: 0 } }, * { user: { name: 'John' } } * ) // 返回: { user: { name: 'John', age: 0 } } * ``` * * @example 键名映射 * ```typescript * // 静态键名映射 * defaultTemplate( * { a: new TemplateRename('newA', 10) }, * { a: '20' } * ) // 返回: { newA: 20 } * * // 动态键名映射 * defaultTemplate( * { '?': new TemplateRename(key => `prefix_${key}`, true) }, * { foo: false, bar: null } * ) // 返回: { prefix_foo: false, prefix_bar: true } * ``` * * @example 复杂嵌套结构 * ```typescript * const template = { * users: [{ * id: new TemplateRename('userId', ''), * profile: { * name: '', * settings: { '?': false } * } * }] * }; * const data = { * users: [{ * id: 'u123', * profile: { * name: 'John', * settings: { notifications: null, darkMode: true } * } * }] * }; * // 返回: { * // users: [{ * // userId: 'u123', * // profile: { * // name: 'John', * // settings: { notifications: false, darkMode: true } * // } * // }] * // } * ``` * * @param def 模板定义,指定默认值和类型。可以是: * - 基本类型值:用作默认值和类型示例 * - 数组:第一个元素作为所有数组元素的模板 * - 对象:键值对定义属性的模板,'?' 键作为通配符模板 * - TemplateRename:定义键名映射规则 * - null/undefined:表示保持原值不变 * @param src 源数据,需要处理的原始数据 * @param opt 选项配置,可以自定义通配符键名 * @returns 处理后的数据 */ export default function defaultTemplate<T>(def: T, src: any, opt: DefaultTemplateOption = { allTemplateKey: '?' }): TemplateResult<T> { return internalTemplate(def, src, opt, undefined) as TemplateResult<T>; } /** * 内部模板处理函数,处理模板定义和源数据的递归转换 * * 处理流程: * 1. 解包 TemplateRename: * - 递归处理嵌套的 TemplateRename * - 应用键名转换函数 * 2. 基础情况处理: * - null/undefined 模板:返回原值 * - 函数模板:直接调用 * 3. 类型处理: * - 基本类型:调用 convertToType 进行类型转换 * - 数组:递归处理每个元素 * - 对象:调用 handleObjectTemplate 处理 * * @param def 模板定义 * @param src 源数据 * @param opt 选项配置 * @param owner 当前处理的对象或数组,用于键名转换函数 * @returns 处理后的数据 * @internal */ function internalTemplate(def: any, src: any, opt: DefaultTemplateOption, owner: any): any { // 递归解包 TemplateRename while (def instanceof TemplateRename) { const key = typeof def.key === 'function' && owner ? def.key(owner) : def.key; def = def.def; owner = key; } if (def == null) return src; if (typeof def === 'function') return def(src, owner); if (isBaseType(def)) { return src != null && isBaseType(src) ? convertToType(def, src) : def; } if (Array.isArray(def)) { if (Array.isArray(src)) { if (def.length > 0) { // 判断模板元素是否为对象模板 if (isObject(def[0])) { return src.map(item => { // 只保留模板字段,且递归时不传原 item,防止多余字段混入 return handleObjectTemplate(def[0], item, opt, undefined); }); } else { return src.map(item => internalTemplate(def[0], item, opt, src)); } } return src; } return []; } if (isObject(def)) { return handleObjectTemplate(def, src, opt, owner); } return def; } /** * 处理对象类型的模板转换 * * 处理流程: * 1. 处理源对象中存在的属性: * - 优先使用属性对应的模板 * - 如无对应模板,尝试使用通配符模板 * - 处理可能的键名重命名 * 2. 处理模板中存在但源对象中不存在的属性: * - 使用模板值作为默认值 * - 处理可能的键名重命名 * 3. 键名重命名处理: * - 解包嵌套的 TemplateRename * - 支持静态重命名和动态重命名函数 * * 注意事项: * - 通配符键 ('?') 只用于未显式定义模板的属性 * - 重命名后的键名不会再次应用模板 * - 源对象中的属性如果没有对应模板且无通配符模板会被忽略 * * @param def 对象类型的模板定义 * @param src 源数据 * @param opt 选项配置 * @param owner 当前处理的父对象 * @returns 处理后的对象 * @internal */ function handleObjectTemplate(def: PlainObject, src: any, opt: DefaultTemplateOption, owner: any): PlainObject { const allKey = opt.allTemplateKey || '?'; const result: PlainObject = {}; const defKeys = Object.keys(def); const srcObj = isObject(src) ? src : {}; // 处理模板中声明的字段 for (const key of defKeys) { if (key === allKey) continue; let tpl = def[key]; let newKey = key; while (tpl instanceof TemplateRename) { newKey = typeof tpl.key === 'function' ? tpl.key(key) : tpl.key; tpl = tpl.def; } const val = internalTemplate(tpl, srcObj[key], opt, srcObj); if (val !== undefined) result[newKey] = val; } // 处理通配符模板,补全原对象中未被模板显式声明的字段 if (def[allKey] !== undefined) { for (const key of Object.keys(srcObj)) { if (def.hasOwnProperty(key)) continue; let tpl = def[allKey]; let newKey = key; while (tpl instanceof TemplateRename) { newKey = typeof tpl.key === 'function' ? tpl.key(key) : tpl.key; tpl = tpl.def; } const val = internalTemplate(tpl, srcObj[key], opt, srcObj); if (val !== undefined) result[newKey] = val; } } // 处理模板中存在但原对象中不存在的字段 for (const key of defKeys) { if (key === allKey || srcObj.hasOwnProperty(key)) continue; let tpl = def[key]; let newKey = key; while (tpl instanceof TemplateRename) { newKey = typeof tpl.key === 'function' ? tpl.key(key) : tpl.key; tpl = tpl.def; } const val = internalTemplate(tpl, undefined, opt, srcObj); if (val !== undefined) result[newKey] = val; } return result; }