我需要编写一个函数,通过向对象树中的每个 node 添加访问控制字段$来为对象生成注释. 要注释对象{ field: {...subFields} },我们需要获取{ field: { $: 'view', ...subFieldsAnnotated } }. 要注释数组及其元素,只要有类似{ arr: [...elements] }的内容,我们就应该用{ arr: { $: 'view', $items: [...elementsAnnotated] } }替换它.

我想做以下工作:

type Annotated<T, NewFieldName extends string, NewFieldValue> =
    T extends Array<infer U>
        ? // Wrap arrays in an object with '$items' and the new field.
          {
              $items: Array<Annotated<U, NewFieldName, NewFieldValue>>;
          } & {
              [K in NewFieldName]: NewFieldValue;
          }
        : // Check if T is an object (but not an array, which is handled above).
          T extends object
          ? {
                [P in keyof T]: Annotated<T[P], NewFieldName, NewFieldValue>;
            } & { [K in NewFieldName]: NewFieldValue }
          : // Directly create an object with the new field for primitives.
            { [K in NewFieldName]: NewFieldValue };

type Access = "none" | "view" | "edit";

type Accessor<T> = Annotated<T, "$", Access>;

function isObject<T>(value: T): boolean {
    return value !== null && typeof value === "object";
}

// this is okay
const emptyObjAnnotated: Accessor<{}> = {
    $: "view",
};

// this in not
function getAccessor<T>(value: T, access: Access): Accessor<T> {
    if (isObject(value)) {
        if (!Array.isArray(value)) {
            // >> ERROR: should extend T with '{}' or 'object'
            const keys = Object.keys(value) as (keyof typeof value)[];
            // >> ERROR: Type '{ $: Access; }' is not assignable to type 'Annotated<T, "$", Access>'
            return {
                $: access,
                ...keys.reduce((acc, key) => {
                    if (isObject(value[key])) {
                        return {
                            ...acc,
                            [key]: getAccessor(value[key], access),
                        };
                    }
                    return {
                        ...acc,
                        [key]: { $: access },
                    };
                }, {}),
            };
        }
        if (Array.isArray(value)) {
            // >> ERROR: Type '{ $: Access; $items: { $: Access; }[]; }' is not assignable to type 'Annotated<T, "$", Access>'
            return {
                $: access,
                $items: value.map((child) => {
                    if (isObject(child)) {
                        return getAccessor(child, access);
                    }
                    return { $: access };
                }),
            };
        }
    }

    // ERROR: Type '{ $: Access; }' is not assignable to type 'Annotated<T, "$", Access>'
    return { $: access };
}

// // for testing purposes
// 
// type ExampleType = {
//     id: number;
//     name: string;
//     tags: string[];
//     details: {
//         description: string;
//         images: {
//             url: string;
//             alt: string;
//         }[];
//     };
// };
// 
// const example: ExampleType = {
//     id: 1,
//     name: "name",
//     tags: ["tag1", "tag2"],
//     details: {
//         description: "description",
//         images: [
//             {
//                 url: "url",
//                 alt: "alt",
//             },
//         ],
//     },
// };
// 
// const access = getAccessor(example, "view");
// 
// console.log(access.id.$); // 'view'
// console.log(access.details.$); // 'view'
// console.log(access.details.images.$items[0].url.$); // 'view'

问题似乎是,当在子类上递归时,类型泛型Annotated的当前版本不接受类型{ [K in NewFieldName]: NewFieldValue; }.

算法本身似乎是正确的,所以当我将函数的签名更改为

function getAccessor(obj: any, access: Access): Accessor<typeof obj>
// .....

它给出了预期的结果,但是,当然,我会失go 所有的类型表示法,所以我将无法引用基于Accessor<T>的数据 struct 的字段.

你有什么建议吗?我怎么才能打捞到这些人呢?

Playground

推荐答案

getAccessor()函数中,预期的返回类型Accessor<T>类型是一个复杂的generic类型,它使用了conditional typesmapped types和递归. 基本上,编译器不希望查看一些代码并准确地决定是否可以安全地将某个值分配给泛型TAccessor<T>.

目前,它甚至在你最基本的期望中失败了,那就是它会识别出,比如说,isObject(value)以某种方式影响T.不过,在TypeScript 5.4中,我们没有microsoft/TypeScript#33014microsoft/TypeScript#33912.Control flow analysisif (isObject(value)) {⋯} else {⋯}一样只会影响value的表观类型,对T本身没有影响.第Accessor<T>章不受影响 这可能会改变(事实上,它在TypeScript 5.5路由图中根据microsoft/TypeScript#57475,但现在它是一个限制.

然而,即使这个问题得到解决,你也会陷入困境.编译器无法理解reduce()会慢慢地将对象构建成可分配给映射类型的对象.这可能需要像microsoft/TypeScript#1213express中所要求的那样的higher kinded types.另请参阅How can i move away from a Partial<T> to T without casting in Typescript.

我可以继续,但我不认为这是值得的,本质上编译器几乎无法遵循你正在做的任何事情. 因此,试图让它这样做根本没有意义.


对于像这样的情况,您有一个复杂的泛型调用签名和复杂的实现,而编译器无法匹配它们,通常的方法是自己承担验证实现的负担(即,仔细阅读并仔细测试它,并说服自己,对于所有你关心的用例,输出都可以分配给返回类型),然后在必要的地方放松编译器判断以 suppress 错误.

这里最简单的方法是将函数分为从外部看的调用签名和从内部看的松散实现,并将其写成single—call—signature overloaded function:

// call signature
function getAccessor<T>(value: T, access: Access): Accessor<T>;

// implementation
function getAccessor(value: any, access: Access) {
  ⋮ same impl
}

实现应该不需要做太多更改;我已经给了valuethe any type来告诉编译器不要为类型判断而烦恼.一旦这样做了,调用者仍然会看到您想要的强泛型类型.实现错误也就消失了.

不,它并不完美,但我怀疑我们永远不会看到一个版本的TypeScript能够遵循函数中的逻辑,并自动验证它是否匹配一个通用的递归映射条件类型.

Playground link to code

Typescript相关问答推荐

如何使用另一个类型的属性中的类型

TS 2339:属性切片不存在于类型DeliverableSignal产品[]上>'

React Hook中基于动态收件箱定义和断言回报类型

TypeScript Page Routing在单击时不呈现子页面(React Router v6)

使Typescribe强制所有对象具有相同的 struct ,从两个选项

向NextAuth会话添加除名称、图像和ID之外的更多详细信息

TypeScrip-将<;A、B、C和>之一转换为联合A|B|C

在包含泛型的类型脚本中引用递归类型A<;-&B

在实现自定义主题后,Angular 不会更新视图

ANGLING v17 CLI默认设置为独立:延迟加载是否仍需要@NgModule进行路由?

布局组件中的Angular 17命名路由出口

使用条件类型的类型保护

TypeScrip错误:为循环中的对象属性赋值

TypeScrip-根据一个参数值验证另一个参数值

创建一个TypeScrip对象并基于来自输入对象的约束定义其类型

Rxjs将省略的可观测对象合并到一个数组中

如何使用 Angular 确定给定值是否与应用程序中特定单选按钮的值匹配?

对于通过 Axios 获取的数据数组,属性map在类型never上不存在

带有 Select 器和映射器的Typescript 泛型

是否可以使用元组中对象的键来映射类型