【问题标题】:Typescript recursively mapping object property types: object and array element typesTypescript 递归映射对象属性类型:对象和数组元素类型
【发布时间】:2018-04-12 10:54:43
【问题描述】:

更新 1 -> 见下文

给定以下接口

interface Model {
    bla: Child1;
    bwap: string;
}
interface Child1 {
    blu: Child2[];
}
interface Child2 {
    whi: string;
}

我正在尝试编写一个基本上可以像这样使用的函数:

let mapper = path<Model>("bla")("blu");
let myPropertyPath = mapper.path; //["bla", "blu"]

函数调用 x 仅接受属于对象层次结构中 x 级别的键类型的参数。

我有一个工作版本,灵感来自 RafaelSalguero 的帖子 here,它看起来像这样:

interface IPathResult<TRoot, TProp> {
    <TSubKey extends keyof TProp>(key: TSubKey): IPathResult<TRoot, TProp[TSubKey]>;
    path: string[];
}

function path<TRoot>() {
    return <TKey extends keyof TRoot>(key: TKey) => subpath<TRoot, TRoot, TKey>([], key);
}

function subpath<TRoot, T, TKey extends keyof T>(parent: string[], key: TKey): IPathResult<TRoot, T[TKey]> {
    const newPath = [...parent, key];
    const x = (<TSubKey extends keyof T[TKey]>(subkey: TSubKey) => subpath<TRoot, T[TKey], TSubKey>(newPath, subkey)) as IPathResult<TRoot, T[TKey]>;
    x.path = newPath;
    return x;
}

更多的上下文是为了了解我的目标:我正在编写一个与 (p)react 一起使用的绑定库。从 Abraham Serafino 的指南中汲取灵感,发现 here:他使用普通的虚线来描述路径;我希望在执行此操作时具有类型安全性。 所以IPathResult&lt;TRoot, TProp&gt; 实例可以读作如下:这给了我一个从TRoot 开始的路径,它导致任何属性都在TProp 类型的行下方。

它的使用示例是(没有实现细节)

<input type="text" {...bind.text(s => s("bwap")) } />

其中s 参数是path&lt;TState&gt;()TState = Modelbind.text 函数将返回一个 value 属性和一个 onChange 处理程序,该处理程序将调用组件的 setState 方法,将通过我们映射的路径找到的值设置为新值。

这一切都非常有效,直到您将数组属性混入其中。 假设我想为在Child1 接口上的blu 属性中找到的所有元素呈现输入。我想做的是写这样的东西:

let mapper = path<Model>("bla"); //IPathResult<Model, Child1>
for(let i = 0; i < 2; i++) { //just assume there are 2 elements in that array
    let myPath = mapper("blu", i)("whi"); //IPathResult<Model, string>, note: we're writing "whi", which is a key of Child2, NOT of Array<Child2>
}

因此,映射器调用将有一个重载,它为数组TProp 提供一个额外的参数,指定要绑定到的数组中元素的索引,以及最重要的部分:下一个映射器调用将需要数组 element 类型的键,而不是数组类型本身!

因此,将其插入到实际的绑定示例中,可能会变成:

<input type="text" {...bind.text(s => s("bla")("blu", i)("whi")) } />

所以这就是我卡住的地方:即使不考虑实际实现,我如何在IPathResult&lt;TRoot, TProp&gt; 接口中定义该重载?以下是它的要点,但不起作用:

interface IPathResult<TRoot, TProp> {
    <TSubKey extends keyof TProp>(key: TSubKey): IPathResult<TRoot, TProp[TSubKey]>;
    <TSubKey extends keyof TProp>(key: TSubKey, index: number): IPathResult<TRoot, TProp[TSubKey][0]>;
    path: string[];
}

Typescript 不接受 TProp[TSubKey][0] 部分,表示 type 0 cannot be used to index type TProp[TSubKey]。我想这是有道理的,因为打字系统无法知道 TProp[TSubKey] 可以这样索引,它根本没有定义。

如果我写一个这样的界面:

interface IArrayPathResult<TRoot, TProp extends Array<TProp[0]>> {
    <TSubKey extends keyof TProp[0]>(key: TSubKey): IPathResult<TRoot, TProp[0][TSubKey]>;
    path: string[];
}

这让我可以实际输入:

const result: IArrayPathResult<Model, Child2[]> = null;
result("whi"); //woohoow!

当然,这本身就是一个毫无意义的例子,但只是表明可以返回该数组元素类型。但是TProp 现在被定义为始终是一个数组,显然不会是这样。

我觉得我已经很接近了,但是将它们放在一起却让我望而却步。 有什么想法吗?

重要提示:没有实例参数可用于推断类型,也不应该有任何实例参数!

更新 1:

使用 Titian 的 solution 和条件类型,我可以使用我想要的语法。 但是,某些类型信息似乎丢失了:

const mapper = path<Model>();
const test: IPathResult<Model, string> = mapper("bla"); //doesn't error!

这应该不起作用,因为mapper("bla") 调用返回IPathResult&lt;Model, Child1&gt;,它不能分配给IPathResult&lt;Model, string&gt;! 如果您删除 IPathResult 接口中的 2 个方法重载之一,上面的代码 确实 正确地出错,说 “类型 'Child1' 不能分配给类型 'string'。”

如此接近!还有什么想法吗?

【问题讨论】:

    标签: javascript reactjs typescript data-binding


    【解决方案1】:

    您可以使用条件类型来提取item元素,并确保仅使用数组键调用数组版本:

    type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp? P : never}[keyof T];
    type ElementTypeIfArray<T> = T extends Array<infer U> ? U : never
    
    interface IPathResult<TRoot, TProp> {
        <TSubKey extends KeysOfType<TProp, Array<any>>>(key: TSubKey, index: number): IPathResult<TRoot, ElementTypeIfArray<TProp[TSubKey]>>;
        <TSubKey extends keyof TProp>(key: TSubKey): IPathResult<TRoot, TProp[TSubKey]>;
        path: string[];
    }
    
    let mapper = path<Model>()("bla")("blu", 1)("whi"); // works
    let mapper2 = path<Model>()("bla", 1) // error bla not an array key
    

    更新

    不确定为什么 ts 认为 IPathResult&lt;Model, string&gt;IPathResult&lt;Model, Child1&gt; 兼容,如果我删除任何重载,它确实会报告兼容性错误。也许这是一个编译器错误,如果有机会我可能会在以后调查它。最简单的解决方法是向IPathResult 添加一个直接使用TProp 的字段以确保不兼容:

    interface IPathResult<TRoot, TProp> {
        <TSubKey extends KeysOfType<TProp, Array<any>>>(key: TSubKey, index: number): IPathResult<TRoot, ElementTypeIfArray<TProp[TSubKey]>>;
        <TSubKey extends keyof TProp>(key: TSubKey): IPathResult<TRoot, TProp[TSubKey]>;
        path: string[];
        __: TProp;
    }
    

    【讨论】:

    • 该死的!还没有很好地浏览新功能!谢谢老兄,太棒了,正是我想要的!
    • 我说得太早了,请再看看我的帖子,“更新1”。这么近!
    • 啊,是的,打字稿和未使用的泛型在一半的时间里都不能很好地发挥作用,真的 :) 虽然看到智能感知确实报告 IPathResult&lt;Model, Child1&gt; 被返回,但它仍然很奇怪,所以类型肯定是已知的。如果你发现了什么,请告诉我,我也会再看看。它可能会成为 typescript github 论坛的东西。同时,我将此标记为已接受的答案;那个额外的属性真的不会妨碍任何事情:)
    猜你喜欢
    • 2018-04-04
    • 1970-01-01
    • 2021-06-19
    • 1970-01-01
    • 2023-04-01
    • 2016-09-15
    • 2021-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多