Typescript递归映射对象属性类型:对象和数组元素类型

时间:2018-04-12 10:54:44

标签: javascript reactjs typescript data-binding

更新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)反应一起使用的绑定库。从Abraham Serafino的指南中汲取灵感,发现here:他使用普通的虚线来描述一条路径;这样做时我想要类型安全。 因此,IPathResult<TRoot, TProp>实例可以读取为:这为我提供了一条从TRoot开始的路径,该路径会导致TProp类型的任何属性。

其用法的一个例子是(没有实现细节)

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

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

这一切都很有效,直到你在混合中抛出数组属性。 假设我想为blu接口上的Child1属性中找到的所有元素呈现输入。我想做的是写下这样的东西:

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提供额外的参数,指定要绑定到的数组中元素的索引,以及最重要的部分:下一个映射器调用将需要数组元素类型的键,而不是数组类型本身!

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

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

所以这就是我遇到的问题:即使忽视实际的实现,我如何在IPathResult<TRoot, TProp>接口中定义该重载?以下是它的要点,但不起作用:

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<Model, Child1>,而IPathResult<Model, string>无法分配给IPathResult! 如果您在import signal import time class GracefulKiller: def __init__(self): signal.signal(signal.SIGTERM, self.exit_gracefully) self.kill_now = False def exit_gracefully(self, signum, frame): self.kill_now = True def run_something(self): print("starting") time.sleep(5) print("ending") if __name__ == '__main__': killer = GracefulKiller() print(os.getpid()) while True: killer.run_something() if killer.kill_now: break print("End of the program. I was killed gracefully :)") 界面中删除了2个方法重载中的一个,则上面的代码正确错误,说&#34;键入&#39; Child1&#39;不能指定为&#39; string&#39;。&#34;

如此接近!还有更多想法吗?

1 个答案:

答案 0 :(得分: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<Model, string>IPathResult<Model, Child1>兼容,如果我删除任何重载,它确实会报告兼容性错误。也许这是一个编译器错误,如果我有机会,我可能会在以后调查它。最简单的方法是向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;
}