【发布时间】: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<TRoot, TProp> 实例可以读作如下:这给了我一个从TRoot 开始的路径,它导致任何属性都在TProp 类型的行下方。
它的使用示例是(没有实现细节)
<input type="text" {...bind.text(s => s("bwap")) } />
其中s 参数是path<TState>() 和TState = Model。
bind.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<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 接口中的 2 个方法重载之一,上面的代码 确实 正确地出错,说 “类型 'Child1' 不能分配给类型 'string'。”
如此接近!还有什么想法吗?
【问题讨论】:
标签: javascript reactjs typescript data-binding