简体   繁体   English

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

[英]Typescript recursively mapping object property types: object and array element types

UPDATE 1 -> SEE BELOW 更新1 - >见下文

given the following interfaces 给出以下接口

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

I am trying to write a function that will essentially be used like so: 我正在尝试编写一个基本上像这样使用的函数:

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

Function call x only accepts arguments that are keys of the type found at level x in the object hierarchy. 函数调用x仅接受在对象层次结构中在级别x处找到的类型的键的参数。

I have a working version of this, inspired by RafaelSalguero's post found here , and it looks like this: 我有一个这样的工作版本,灵感来自RafaelSalguero在这里找到的帖子,它看起来像这样:

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;
}

A little more context is in order here to see where I'm going with this: I'm writing a binding library for use with (p)react. 这里有更多的上下文来看看我要去哪里:我正在编写一个与(p)反应一起使用的绑定库。 Drawing inspiration from Abraham Serafino's guide, found here : his uses plain dotted strings to describe a path; 从Abraham Serafino的指南中汲取灵感,在这里找到:他使用普通的虚线来描述一条路径; I want to have type safety when doing it. 这样做时我想要类型安全。 So an IPathResult<TRoot, TProp> instance could be read like: this gives me a path starting from TRoot that results in any property down the line of type TProp . 因此,一个IPathResult<TRoot, TProp>实例可以读取这样的:这让我从开始的一条路径TRoot导致任何财产俯视型线TProp

An example of its usage would be (without the implementation details) 其用法的一个例子是(没有实现细节)

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

where the s parameter is path<TState>() and TState = Model . 其中s参数是path<TState>()TState = Model The bind.text function would return a value property and an onChange handler that would call the component's setState method, setting the value found through the path we mapped to the new value. bind.text函数将返回一个value属性和一个onChange处理程序,它将调用组件的setState方法,设置通过我们映射到新值的路径找到的值。

This all works wonderfully, until you throw array properties in the mix. 这一切都很有效,直到你在混合中抛出数组属性。 Let's say I want to render inputs for all elements found in the blu property on the Child1 interface. 假设我想为Child1接口上的blu属性中找到的所有元素渲染输入。 What I would like to do is to write something like this: 我想做的是写这样的东西:

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>
}

So, the mapper calls would have an overload that takes an extra argument for array TProp , specifying the index of the element in the array to bind to, and the most important part: the next mapper call would require keys of the array element type, NOT the array type itself! 因此,映射器调用会有一个重载,它为数组TProp提供额外的参数,指定要绑定的数组中元素的索引,以及最重要的部分: 下一个映射器调用需要数组元素类型的键,不是数组类型本身!

So plugging that into the actual binding example, this could become: 所以将其插入到实际的绑定示例中,这可能会变成:

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

So this is where I'm stuck: even disregarding actual implementation, how do I define that overload in the IPathResult<TRoot, TProp> interface? 所以这就是我被困住的地方:即使忽视实际的实现,我如何在IPathResult<TRoot, TProp>接口中定义该重载? The following would be the gist of it, but doesn't work: 以下是它的要点,但不起作用:

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 doesn't accept the TProp[TSubKey][0] part, saying that type 0 cannot be used to index type TProp[TSubKey] . Typescript不接受TProp[TSubKey][0]部分,说type 0 cannot be used to index type TProp[TSubKey] I guess that makes sense since there is no way for the typing system to know that TProp[TSubKey] can be indexed that way, it's simply not defined. 我想这是有道理的,因为打字系统无法知道TProp[TSubKey] 可以这样索引,它根本就没有定义。

If I write an interface like this: 如果我写这样的界面:

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

that allows me to actually type this: 这允许我实际输入:

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

That's a pointless example in itself of course, but just goes to show that it IS possible to get that array element type returned. 当然,这本身就是一个毫无意义的例子,但只是表明可以获得返回的数组元素类型。 But TProp is now defined as always being an array, which is obviously not going to be the case. 但是TProp现在被定义为始终是一个数组,显然情况并非如此。

I feel like I'm pretty close to getting this, but bringing it all together is eluding me. 我觉得我已经非常接近这一点了,但把它们放在一起就是在逃避我。 Any ideas? 有任何想法吗?

Important note : there are no instance arguments to infer types from, and there aren't supposed to be any! 重要提示 :没有实例参数来推断类型,并且不应该有任何!

UPDATE 1: 更新1:

Using Titian's solution with conditional types, I can use the syntax I wanted. 使用带有条件类型的Titian 解决方案 ,我可以使用我想要的语法。 However, some type information seems to have gone missing: 但是,某些类型信息似乎已丢失:

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

This should not work, because the mapper("bla") call returns IPathResult<Model, Child1> , which is not assignable to IPathResult<Model, string> ! 这应该不起作用,因为mapper("bla")调用返回IPathResult<Model, Child1> ,它不能分配给IPathResult<Model, string> If you remove one of the 2 method overloads in the IPathResult interface, the code above does correctly error, saying that "Type 'Child1' is not assignable to type 'string'." 如果你删除IPathResult接口中的2个方法重载之一,上面的代码正确地出错,说“类型'Child1'不能分配给'string'类型。”

So close! 很近! Any more ideas? 还有什么想法吗?

You can use conditional types to extract the item element, and ensure that the array version is only called with array keys: 您可以使用条件类型来提取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

Update 更新

Not sure why ts thinks IPathResult<Model, string> is compatible with IPathResult<Model, Child1> , if I remove the any of the overloads it indeed does report a compatibility error. 不确定为什么ts认为IPathResult<Model, string>IPathResult<Model, Child1> IPathResult<Model, string>兼容,如果我删除了任何重载,它确实报告了兼容性错误。 Maybe it is a compiler bug, I might investigate it later if I get a chance. 也许这是一个编译器错误,如果我有机会,我可能会在以后调查它。 The simplest work around is to add a field to IPathResult that uses TProp directly to ensure incompatibility: 最简单的方法是向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;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM