TypeScript:接口,取决于(递归)属性的类型

时间:2020-09-17 19:29:30

标签: typescript typescript-typings

我有一个类似树的数据结构。我想用节点包装器包装每个树节点,这为节点添加了附加功能。但是,与此同时,我想保持输入的完整性。有人可以帮我吗?

这里是一个示例,展示了我想要实现的目标

const a = { children: [], value: 12 } as Node<number>;
const b = { children: [], value: 'string' } as Node<string>;
const c = { children: [b, a], value: true } as Node<boolean>;

const root = { children: [c]; value: null } as Node<null>;

// wrap node data with some functions, but keep typings stable
const wrappedNode = wrapNode(root);

wrappedNode.value; // type should be => null
wrappedNode.children[0].value; // type should be => boolean
wrappedNode.children[0].children[0].value; // type should be => string
wrappedNode.children[0].children[1].value; // type should be => number

我当前的方法如下:

interface Node<T, C extends Node<unknown, any[]>[]> {
  children: [...C];
  value: T;
}

interface WrapNode<T, C extends Node<unknown, any[]>[]> {
  children: WrapNode<any, [...C]>[];
  value: T;
  computeValue(): any;
}

function createNode<T, C extends Node<unknown, any[]>[]>(value: T, children: [...C]): Node<T, [...C]> {
  return {
    value,
    children,
  };
}

export function wrapNode<T, C extends Node<any, any>[]>(node: Node<T, C>): WrapNode<T, typeof node.children> {
  const value = node.value;

  return {
    ...node,
    computeValue: () => value,
    children: node.children.map(child => wrapNode(child)),
    //                          ^^^^^    ^^^^^^^^^^^^^^^
    //           types are:   C[number]  wrapNode<any, any>(n: Node<any, any>)
  };
}

const a = createNode(12, []);
const b = createNode('str', []);
const c = createNode(null, [b, a]);

const x = wrapNode(c);

x.value; // gives me type null, ok!
x.children[0].value; // gives me any :(
x.children[1].value; // gives me any too :(

使用TypeScript甚至可能吗?我正在使用TypeScript 4.0.2,如果有帮助的话。在此先感谢:)

1 个答案:

答案 0 :(得分:4)

我认为您可能希望您的WrapNode界面看起来像这样:

interface WrapNode<T, C extends Node<unknown, any[]>[]> {
    children: { [K in keyof C]:
        C[K] extends Node<infer CT, infer CC> ? WrapNode<CT, CC> : never
    }
    value: T,
    computeValue(): any,
}

这里的重要区别是children属性是一个mapped array/tuple type,它遍历children的数字索引并获取每个元素的类型。

编译器实际上没有太多机会验证任何特定的值都可以分配给这种映射类型,因此您可能希望在{{1 }}实现:

wrapNode()

然后,一切都会按照您想要的方式工作,

function wrapNode<T, C extends Node<unknown, any[]>[]>(node: Node<T, C>): WrapNode<T, C> {
    const value = node.value;
    return {
        ...node,
        computeValue: () => value,
        children: node.children.map(wrapNode) as any, // need to assert here
    };
}

type assertion