给定一个简单的对象树,其中包含其自身类型的值或需要转换的类型的值:
interface Tree<Leaf> {
[key: string]: Tree<Leaf> | Leaf;
}
Run Code Online (Sandbox Code Playgroud)
我想定义一个函数,将所有叶子递归地转换为另一种类型,如下所示:
function transformTree<S, T>(
obj: Tree<S>,
transform: (value: S) => T,
isLeaf: (value: S | Tree<S>) => boolean
): Tree<T> {
return Object.assign(
{},
...Object.entries(obj).map(([key, value]) => ({
[key]: isLeaf(value)
? transform(value as S)
: transformTree(value as Tree<S>, transform, isLeaf),
}))
);
}
Run Code Online (Sandbox Code Playgroud)
如何维护源树和转换树之间叶子的类型?
测试以上不起作用:
class Wrapper<T> {
constructor(public value: T) {}
}
function transform<T>(wrapped: Wrapper<T>): T {
return wrapped.value;
}
function unwrap<T>(wrapped: Tree<Wrapper<T>>): Tree<T> {
return transformTree<Wrapper<T>, T>(
wrapped,
transform,
(value: Wrapper<T> | Tree<Wrapper<T>>) => value instanceof Wrapper
);
}
const obj = unwrap<string>({
foo: {
bar: new Wrapper("baz"),
},
cow: new Wrapper("moo"),
});
function handleBaz(value: "baz") {
return true;
}
function handleMoo(value: "moo") {
return true;
}
handleBaz(obj.foo.bar); // Error:(162, 19) TS2339: Property 'bar' does not exist on type 'string | Tree<string>'. Property 'bar' does not exist on type 'string'.
handleMoo(obj.cow); //Error:(163, 11) TS2345: Argument of type 'string | Tree<string>' is not assignable to parameter of type '"moo"'. Type 'string' is not assignable to type '"moo"'.
Run Code Online (Sandbox Code Playgroud)
我可以看到发生了错误,因为树结构不是通过转换维护的(转换仅在运行时起作用)。但鉴于输入树的已知结构,转换是可预测的,我认为应该有一种方法可以在打字稿中做到这一点。
这是我解决这个问题的尝试:
type TransformTree<Leaf, InputTree extends Tree<Leaf>, T> = {
[K in keyof InputTree]: InputTree[K] extends Leaf
? T
: InputTree[K] extends Tree<Leaf>
? TransformTree<Leaf, InputTree[K], T>
: never;
};
type IsLeaf<Leaf> = (value: Tree<Leaf> | Leaf) => boolean;
function transformTree<T extends Tree<From>, From, To>(
tree: T,
transform: (value: From) => To,
isLeaf: IsLeaf<From>
): TransformTree<From, typeof tree, To> {
return Object.assign(
{},
...Object.entries(tree).map(([key, value]) => ({
// XXX have to cast the value in each case, because typescript cannot predict
// the outcome of isLeaf().
[key]: isLeaf(value)
? transform(value as Extract<typeof tree[typeof key], From>)
: transformTree(
value as Extract<typeof tree[typeof key], Tree<From>>,
transform,
isLeaf
),
}))
);
}
Run Code Online (Sandbox Code Playgroud)
它似乎仍然不理解嵌套类型:
function unwrap<T>(
wrapped: Tree<Wrapper<T>>
): TransformTree<Wrapper<T>, typeof wrapped, T> {
return transformTree(
wrapped,
transform,
(value: Wrapper<T> | Tree<Wrapper<T>>) => value instanceof Wrapper
);
}
function handleBaz(value: "baz") {
return true;
}
function handleMoo(value: "moo") {
return true;
}
handleMoo(obj.cow); // OK
handleBaz(obj.foo.bar); // TS2339: Property 'bar' does not exist on type 'never'.
Run Code Online (Sandbox Code Playgroud)
似乎打字稿仍然认为如果字段值不是叶子,那么它可能不是子树。
接下来,我将只关心打字而不是实现。所有函数都将被declare编译,就好像实际的实现在某个 JS 库中,并且这些是它们的声明文件。
此外,您的isLeaf函数可能应该被键入为用户定义的类型保护,其返回类型是类型谓词而不仅仅是boolean。函数签名isLeaf: (value: S | Tree<S>) => value is S与返回的函数签名类似boolean,只是编译器实际上会理解 in if (isLeaf(x)) { x } else { x }、xtrue 块中的S和xfalse 块中的Tree<S>。
好的,这里是:
该类型Tree<X>过于笼统,无法跟踪特定的键和值类型。编译器所知道的关于类型值(例如 )的全部信息Tree<string>是,它是一个对象类型,其属性为类型string或Tree<string>。一旦你这样做了,就说:
const x: Tree<string> = { a: "", b: { c: "", d: { e: "" } } };
Run Code Online (Sandbox Code Playgroud)
您已经丢弃了有关特定结构的所有详细信息以及有关string叶子的任何特定子类型的所有详细信息:
x.a.toUpperCase(); // error
x.z; // no error
Run Code Online (Sandbox Code Playgroud)
如果您所关心的只是提出一个类型转换来维护嵌套的键结构并将 的某些子类型转换Tree<X>为具有相同形状的 的子类型Tree<Y>,那么您可以做到。但在最直接的实现中,生成的树的所有叶子都将是类型Y,而不是任何更窄的类型。我是这样写的:
type TransformTree<T extends Tree<X>, X, Y> = { [K in keyof T]:
T[K] extends X ? Y :
T[K] extends Tree<X> ? TransformTree<T[K], X, Y> :
T[K];
};
declare function transformTree<X, Y, TX extends Tree<X>>(
obj: TX,
transform: (value: X) => Y,
isLeaf: (value: X | Tree<X>) => value is X
): TransformTree<TX, X, Y>;
Run Code Online (Sandbox Code Playgroud)
您可以在一个简单的示例中看到它的工作原理,如下所示:
const t1 = { a: "A", b: { c: "CC", d: { e: "EEE" } } };
const t2 = transformTree(t1,
(x: string) => x.length,
(v): v is string => typeof v === "string"
);
t2.a; // number
t2.b.c; // number
t2.b.d.e; // number
Run Code Online (Sandbox Code Playgroud)
但你想要一些更雄心勃勃的东西;似乎您不仅想要从特定类型X到特定类型的叶转换映射Y,而且还想指定一些通用类型函数,例如 type F<T extends X> = ...将叶从类型映射Z extends X到F<Z>。
在您的示例中,您的输入类型类似于Wrapped<any>,您的输出类型函数将类似于type F<T extends Wrapped<any> = T["value"];
不幸的是,这种更通用的转换类型无法用 TypeScript 表达。您不能拥有像type TransformType<T extends Tree<X>, X, F> = ...whereF本身就是带有参数的类型函数这样的类型函数。这需要语言支持所谓的“更高种类的类型”。microsoft/TypeScript#1213对此有一个长期的开放功能请求,虽然拥有它们会令人惊奇,但看起来在可预见的未来不会发生。
您可以做的是设想特定类型的叶类型转换并为它们实现特定版本TransformTree。例如,如果您的叶类型映射仅索引到单个属性(type F<T extends Record<K, any>, K extends PropertyKey> = T[K]如您的Unwrap情况),那么您可以这样编写:
type TransformTreeIdx<T, X, K extends keyof X> = { [P in keyof T]:
T[P] extends X ? X[K] :
TransformTreeIdx<T[P], X, K>;
};
declare function transformTreeIdx<TX, X, K extends keyof X>(
obj: TX,
key: K,
isLeaf: (value: any) => value is X
): TransformTreeIdx<TX, X, K>;
Run Code Online (Sandbox Code Playgroud)
然后使用它:
const w = {
foo: {
bar: new Wrapper("baz" as const),
},
cow: new Wrapper("moo" as const),
};
const w2 = transformTreeIdx(
w, "value", (x: any): x is Wrapper<any> => x instanceof Wrapper
);
handleBaz(w2.foo.bar);
handleMoo(w2.cow);
Run Code Online (Sandbox Code Playgroud)
或者,正如您的评论中提到的,您可能想对特定的通用接口/类执行相反的操作,例如,Wrapper...映射将转换Z extends X为Wrapper<Z>:
type TransformTreeWrap<T, X> = { [P in keyof T]:
T[P] extends X ? Wrapper<T[P]> :
TransformTreeWrap<T[P], X>;
};
declare function transformTreeWrap<TX, X, K extends keyof X>(
obj: TX,
isLeaf: (value: any) => value is X
): TransformTreeWrap<TX, X>;
Run Code Online (Sandbox Code Playgroud)
并使用它:
const u = {
foo: {
bar: "baz" as const,
},
cow: "moo" as const,
};
const u2 = transformTreeWrap(
u, (x: any): x is string => typeof x === "string"
);
handleBaz(u2.foo.bar.value);
handleMoo(u2.cow.value);
Run Code Online (Sandbox Code Playgroud)
或者,您可能有一个isLeaf/对的transform数组,允许针对不同的更具体的转换来测试每个节点。因此,例如,每当您"moo"在树中找到一个值时,您都会输出 a number,而每当您"baz"在树中找到一个值时,您都会输出 a boolean。
那么你的输入可能如下所示:
type TransformTreeMap<T, M extends [any, any]> = { [K in keyof T]:
T[K] extends M[0] ? Extract<M, [T[K], any]>[1] :
TransformTreeMap<T[K], M> };
type IsLeafAndTransformer<I, O> = {
isLeaf: (x: any) => x is I,
transform: (x: I) => O
}
type TransformArrayToMap<M extends Array<IsLeafAndTransformer<any, any>>> = {
[K in keyof M]: M[K] extends IsLeafAndTransformer<infer I, infer O> ?
[I, O] : never }[number]
declare function transformTreeMap<T, M extends Array<IsLeafAndTransformer<any, any>>>(
obj: T,
...transformers: M
): TransformTreeMap<T, TransformArrayToMap<M>>;
Run Code Online (Sandbox Code Playgroud)
然后你使用它:
const mm = transformTreeMap(u,
{ isLeaf: (x: any): x is "moo" => x === "moo", transform: (x: "moo") => 123 },
{ isLeaf: (x: any): x is "baz" => x === "baz", transform: (x: "baz") => true }
);
mm.cow // number
mm.foo.bar // boolean
Run Code Online (Sandbox Code Playgroud)
因此,您确实可以定义很多不同的甚至非常强大的树转换......只是不是问题所暗示的完全通用的高阶类型。希望其中之一能为您指明前进的方向。好的,祝你好运!