我正在输入一种查询引擎.以下是到目前为止的情况:

type WhereClause = { [key: string]: string | number | boolean };

type $Option = { "$"?: { where: WhereClause } };

type Subquery = { [namespace: string]: NamespaceVal };

type NamespaceVal = $Option & Subquery | $Option;

type Query = { [namespace: string]: NamespaceVal };


type InstantObject = {
  id: string;
  [prop: string]: any;
};

type Responsify<T> = {
  [P in keyof T]: T[P] extends object
    ? (Responsify<Omit<T[P], "$">> & { id: string; [k: string]: any })[]
    : T[P][];
};

function queryResponse<T extends Query>(q: T): Responsify<T> {
  return 1 as any
}

const resOne = queryResponse({users: {$: {where: {"foo": 1}}, comments: {}}});

这是可行的,但输出类型非常冗长:

declare const resOne: Responsify<{
    users: {
        $: {
            where: {
                foo: number;
            };
        };
        comments: {};
    };
}>;

这可能会使最终用户很难通过IntelliSense阅读.

问题:

有没有办法,我可以产生一个"更干净"的输出类型?

一个明显的胜利,如果我可以完全省略$:

declare const resOne: Responsify<{
    users: {
        comments: {};
    };
}>;

推荐答案

因此,您希望IntelliSense接受类似Responsify<T>的类型并将其显示为Responsify<{⋯}>,其中内部的类型是T的某个版本,并删除了从第二级开始的任何名为"$"的属性.(我之所以说是第二级,是因为您最初定义的Responsify<T>不会删除键为"$"的顶级属性.我不确定这是否重要,但这是一个小问题.)

您不能真正做到这一点;很难想象编写Responsify<X>并让编译器以某种方式将其求值为其他类型Y的显示Responsify<Y>.这大概意味着,有时Responsify<T>确实扩展了定义,而有些时候则不是.我认为这是不可能的.或者至少我还没找到办法.

我能想象的最接近的事情就是用其他类型来定义Responsify<T>,比如说,_Responsify<T>,这样前者将始终显示后者.所以这意味着我想要这个:

type Responsify<T> = _Responsify<{ [K in keyof T]: DeDollar<T[K]> }>

因此,在这里,_Responsify<T>或多或少是您的原始定义,DeDollar<T>是一种递归地从所有级别删除"$"键的类型.我们可以这样写:

type DeDollar<T> =
  T extends readonly any[] ? { [I in keyof T]: DeDollar<T[I]> } : 
  T extends object ? { [K in keyof T as Exclude<K, "$">]: DeDollar<T[K]> } :
  T

这与使用the Omit utility type非常相似,但通过写出{[K in keyof T as Exclude<K, "$">]: ⋯},我们可以避免在文字显示中看到单词Omit.

基本上就是这样.当然,如果您只对已经被DeDollared的类型调用Responsify,那么_Responsify就不必再担心"$"了:

type _Responsify<T> = {
    [P in keyof T]: T[P] extends object
    ? (_Responsify<T[P]> & { id: string;[k: string]: any })[]
    : T[P][];
};

这应该不会改变任何事情.好的,让我们来测试一下:


const resOne = queryResponse({ users: { $: { where: { "foo": 1 } }, comments: {} } });
//    ^? const resOne: _Responsify<{ users: { comments: {}; }; }>

这就是你想要的.您可以验证此类型是否扩展到与原始版本相同,可能是使用类似于ExpandRecursively的代码,如How can I see the full expanded contract of a Typescript type?:

type ResOne = ExpandRecursively<typeof resOne>;
/* type ResOne = {
    users: {
        [x: string]: any;
        comments: {
            [x: string]: any;
            id: string;
        }[];
        id: string;
    }[];
} */

Playground link to code

Typescript相关问答推荐

如何使用Generics保留构造函数的类型信息?

如何使打包和拆包功能通用?

有没有可能使用redux工具包的中间件同时监听状态的变化和操作

Angular中的其他服务仅获取默认值

类型脚本泛型最初推断然后设置

使用打字Angular 中的通用数据创建状态管理

具有自引用属性的接口

在排版修饰器中推断方法响应类型

如何在方法中正确地传递来自TypeScrip对象的字段子集?

不带其他属性的精确函数返回类型

类型脚本-处理值或返回值的函数

tailwind css未被应用-react

TS2739将接口类型变量赋值给具体变量

作为函数参数的联合类型的键

类型脚本中参数为`T`和`t|unfined`的重载函数

如何在TypeScript中描述和实现包含特定属性的函数?

仅当定义了值时才按值筛选数组

Typescript 从泛型推断类型

递归类型名称

如何在故事书的Typescript 中编写decorator ?