TypeScript泛型函数类型不兼容报错,是否只能用类型断言解决?
TypeScript泛型函数获取子节点的类型错误解决方法
问题描述
我编写了一个TypeScript泛型函数用于遍历树结构获取所有子节点,代码如下:
type TreeNodeValue = string | number | string[] | number[]; type TreeNodeNormal = { value: TreeNodeValue; children?: TreeNodeNormal[]; } export function getAllChildrenNodes<T = TreeNodeNormal>( node: TreeNodeNormal | undefined | null, mapper?: (node: TreeNodeNormal) => T, children: T[] = [] ): T[] { if (!node || !Array.isArray(node.children)) return children; for (const child of node.children) { if (typeof mapper === "function") { children.push(mapper(child)); } else { children.push(child); // 这里触发TS错误 } children = children.concat(getAllChildrenNodes(child, mapper)); } return children; }
触发的TypeScript错误:
Argument of type 'TreeNodeNormal' is not assignable to parameter of type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'TreeNodeNormal'.ts(2345)
疑问:是不是只能通过类型断言children.push(child as unknown as T);来解决这个问题?
补充示例数据:
const treeData = [ { title: 'Node1', value: '0-0', children: [ { title: 'Child Node1', value: '0-0-1', }, { title: 'Child Node2', value: '0-0-2', }, ], }, { title: 'Node2', value: '0-1', }, ]
调用示例:
getAllChildrenNodes({children: treeData}, (node) => node.value) // 返回 ['0-0', '0-0-1', '0-0-2', '0-1'],此时T是string
解决方案:不用类型断言,优化泛型逻辑
完全不需要依赖类型断言就能解决,问题根源在于当前泛型的约束逻辑不够清晰:T的默认值是TreeNodeNormal,但TypeScript无法保证当没有传入mapper时,T一定等于TreeNodeNormal——理论上调用者可以显式指定一个和TreeNodeNormal无关的T,导致类型不兼容。
下面提供两种更合理的解决方式:
方式1:使用函数重载明确调用场景
通过函数重载直接定义两种调用模式,让TypeScript精准推断类型:
type TreeNodeValue = string | number | string[] | number[]; type TreeNodeNormal = { value: TreeNodeValue; children?: TreeNodeNormal[]; } // 重载签名1:不传入mapper,返回TreeNodeNormal数组 export function getAllChildrenNodes( node: TreeNodeNormal | undefined | null, children?: TreeNodeNormal[] ): TreeNodeNormal[]; // 重载签名2:传入mapper,返回映射后的T类型数组 export function getAllChildrenNodes<T>( node: TreeNodeNormal | undefined | null, mapper: (node: TreeNodeNormal) => T, children?: T[] ): T[]; // 函数实现 export function getAllChildrenNodes<T = TreeNodeNormal>( node: TreeNodeNormal | undefined | null, mapper?: (node: TreeNodeNormal) => T, children: T[] = [] ): T[] { if (!node || !Array.isArray(node.children)) return children; for (const child of node.children) { if (typeof mapper === "function") { children.push(mapper(child)); } else { // 此时通过重载推断,T必然是TreeNodeNormal,类型安全 children.push(child as T); } children.push(...getAllChildrenNodes(child, mapper)); } return children; }
方式2:优化泛型约束,结合条件类型
通过条件类型让TypeScript理解T和mapper的关联,同时限制无mapper时的T范围:
type TreeNodeValue = string | number | string[] | number[]; type TreeNodeNormal = { value: TreeNodeValue; children?: TreeNodeNormal[]; } export function getAllChildrenNodes<T = TreeNodeNormal>( node: TreeNodeNormal | undefined | null, mapper?: (node: TreeNodeNormal) => T, children?: T[] ): T[] { const currentChildren = children ?? []; if (!node || !Array.isArray(node.children)) return currentChildren; for (const child of node.children) { if (typeof mapper === "function") { currentChildren.push(mapper(child)); } else { // Extract确保T是TreeNodeNormal的子类型,TypeScript能正确推断 currentChildren.push(child as Extract<T, TreeNodeNormal>); } currentChildren.push(...getAllChildrenNodes(child, mapper)); } return currentChildren; }
额外优化点
原代码中使用children.concat会创建新数组,改用push(...getAllChildrenNodes(...))可以减少不必要的数组实例化,提升性能。
内容的提问来源于stack exchange,提问作者Lin Du
相关产品推荐
相关产品推荐

