You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

TypeScript嵌套数组可空属性转可选工具类型失效问题

TypeScript工具类型无法处理自定义类型数组嵌套可空属性的问题修复

问题描述

  • 编写了一个TypeScript工具类型,目标是将类型中所有可空属性(包括多层嵌套的可空属性)转换为可选属性
  • 异常表现:当属性值为其他自定义类型数组时,工具类型无法正常生效;但数组结构直接内联展开定义在父类型中时,工具类型可以正常运行

原有类型实现

// 判断T是否包含null类型,是则返回null,否则返回原类型
type ExtractNull<T> = Extract<T, null> extends null
    ? Extract<T, null> extends never
        ? T
        : null
    : T
// 提取T中所有值可空的键名联合类型
type NullKeys<T extends Record<PropertyKey, any>> = {
    [K in keyof T]: ExtractNull<T[K]> extends null
        ? K
        : T[K] extends Record<PropertyKey, any>
        ? NullKeys<T[K]>
        : never
}[keyof T]
// 将所有可空属性转为可选属性
export type OptionalNulls<T extends Record<PropertyKey, any>> = {
    [K in keyof T as Exclude<K, NullKeys<T>>]: T[K] extends Record<
        PropertyKey,
        any
    >
        ? OptionalNulls<T[K]>
        : T[K]
} & Partial<{
    [K in keyof T as Extract<K, NullKeys<T>>]: T[K]
}>

测试用例

测试预期:所有值为null | 其他类型的可空属性,无论嵌套多深、是否在数组中,都要被转为可选属性。

type TestRoot = {
    prop0: null | string
    testObj: TestObj
    testObjArray: TestObj[]
    testObjArrayUnwrapped: [
        {
            prop1: null | string
            arr: [
                {
                    prop2: null | string
                }
            ]
        }
    ]
}
type TestObj = {
    prop1: null | string
    arr: [
        {
            prop2: null | string
        }
    ]
}
const test: OptionalNulls<TestRoot> = {
    prop0: "optional",
    testObj: {
        prop1: "optional",
        arr: [{ prop2: "optional" }],
    },
    // 以下属性内的可空属性未被转为可选,不符合预期
    testObjArray: [{ prop1: "not optional", arr: [{ prop2: "not optional" }] }],
    // 内联数组下的可空属性正常转为可选,符合预期
    testObjArrayUnwrapped: [
        {
            prop1: "optional",
            arr: [
                {
                    prop2: "optional",
                },
            ],
        },
    ],
}

缺陷原因

原有类型存在两个核心问题:

  1. 递归逻辑分支缺失:仅对匹配Record<PropertyKey, any>的普通对象类型做嵌套递归处理,完全没有覆盖数组、元组类型。当属性值为TestObj[]这类命名类型数组时,会直接返回原数组类型,不会深入遍历数组元素的内部结构,导致元素内的可空属性没有被转换。
  2. 内联元组生效属于偶然行为:TypeScript对直接内联在父类型中的元组字面量会做隐式的上下文类型推导,自动递归遍历结构,因此看起来符合预期;但提前定义的命名类型数组不会触发这个隐式推导,就会出现表现不一致的问题。

修复方案

新增统一的嵌套类型处理逻辑,覆盖数组、元组、普通对象三类嵌套场景,同时调整可空键的提取逻辑,将数组/元组纳入递归遍历范围:

type ExtractNull<T> = Extract<T, null> extends null
    ? Extract<T, null> extends never
        ? T
        : null
    : T

// 统一处理所有嵌套类型:数组、元组、普通对象
type ProcessNested<T> = 
    // 处理普通数组
    T extends Array<infer U> ? Array<ProcessNested<U>>
    // 处理元组
    : T extends readonly [infer First, ...infer Rest] 
        ? readonly [ProcessNested<First>, ...{ [K in keyof Rest]: ProcessNested<Rest[K]> }]
    // 处理普通对象
    : T extends Record<PropertyKey, any> ? OptionalNulls<T>
    // 基础类型直接返回
    : T

type NullKeys<T extends Record<PropertyKey, any>> = {
    [K in keyof T]: ExtractNull<T[K]> extends null
        ? K
        // 把数组、元组也纳入可空键的递归判断范围
        : T[K] extends Record<PropertyKey, any> | Array<any> | readonly any[]
            ? NullKeys<ProcessNested<T[K]> extends Record<PropertyKey, any> ? ProcessNested<T[K]> : never>
            : never
}[keyof T]

export type OptionalNulls<T extends Record<PropertyKey, any>> = {
    [K in keyof T as Exclude<K, NullKeys<T>>]: ProcessNested<T[K]>
} & Partial<{
    [K in keyof T as Extract<K, NullKeys<T>>]: ProcessNested<T[K]>
}>

修复后testObjArray属性下的prop1、prop2等嵌套可空属性会被正确转为可选,和内联数组的表现完全一致。

内容的提问来源于stack exchange,提问作者ne3zy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 06:09:10