我想根据对象的条件指定类型属性

I want to designate a type according to the conditions of the object property

存在具有层次结构的组织数据。

组织节点分为根、部门和用户。

为对象 属性 类型分配了适合节点数据的类型。

所以,希望有条件附上适合对象类型的节点接口

为了解决这个问题,我尝试了索引类型和条件类型,但都没有成功。

如有任何帮助,我们将不胜感激‍♂️

export enum NodeType {
  root = "root",
  dept = "dept",
  user = "user",
}

interface Node {
  readonly nodeId: string;
  readonly type: NodeType;
  title: string;
  childNodes?: Node[];
}

interface RootNode extends Node {
  type: NodeType.root;
  childNodes?: (DeptNode | UserNode)[];
}

interface DeptNode extends Node {
  type: NodeType.dept;
  childNodes?: (DeptNode | UserNode)[];

  deptId: string;
  companyCode: string;
}

interface UserNode extends Node {
  type: NodeType.user;
  childNodes?: UserNode[];

  employeeNumber: string;
  userPrincipalName: string;
}

/**
 * from hierarchy data to flatting data
 */
function flatting(st: Node) {
  flatCompanyData.push(st);
  if (st.childNodes) {
    st.childNodes.forEach((x) => flatting(x));
  }
}

const companyData: RootNode = {
  nodeId: "0",
  type: NodeType.root,
  title: "Company 1",
  childNodes: [
    {
      nodeId: "0.0",
      type: NodeType.dept,
      title: "Department 1",
      deptId: "A1",
      companyCode: "557",
      childNodes: [
        {
          nodeId: "0.0.0",
          type: NodeType.user,
          title: "User 1",
          employeeNumber: "201911",
          userPrincipalName: "user01@test.com",
        },
        {
          nodeId: "0.0.1",
          type: NodeType.user,
          title: "User 2",
          employeeNumber: "201912",
          userPrincipalName: "user02@test.com",
        },
      ],
    },
    {
      nodeId: "0.1",
      type: NodeType.dept,
      title: "Department 2",
      deptId: "A2",
      companyCode: "558",
    },
  ],
};
let flatCompanyData: Node[] = [];

flatting(companyData);

// I want to be the type of DeptNode interface because the type of object property is NodeType.dept.
const selectDept = flatCompanyData.filter((x) => x.type === NodeType.dept);
selectDept.forEach((x) => {
  console.log({
    nodeId: x.nodeId,
    type: x.type,
    title: x.title,
    // The type is not affirmed in the node that fits the object property type, so you have to affirm the type yourself.
    deptId: (x as DeptNode).deptId,
    companyCode: (x as DeptNode).companyCode,
  });
});

// I want to be the type of UserNode interface because the type of object property is NodeType.user.
const selectUser = flatCompanyData.filter((x) => x.type === NodeType.user);
selectUser.forEach((x) => {
  console.log({
    nodeId: x.nodeId,
    type: x.type,
    title: x.title,
    // The type is not affirmed in the node that fits the object property type, so you have to affirm the type yourself.
    employeeNumber: (x as UserNode).employeeNumber,
    userPrincipalName: (x as UserNode).userPrincipalName,
  });
});

你的代码有两个问题:

首先,一个Node类型的元素不一定是DeptNode类型,因为它有NodeType.dept。考虑以下对象:

{
  nodeId: '0',
  type: NodeType.dept,
  title: 'Company 1'
}

这是一个完全有效的 Node 但不是有效的 DeptNode,因为它缺少字段 deptIdcompanyCode。因此,编译器不将其视为 DeptNode 实际上是正确的。但是有一个解决方案。您可以定义所有可能的节点类型的联合类型,并将其用作 flatCompanyData 的类型,而不是使用 Node 的数组,如下所示:

type NodeUnion = RootNode | DeptNode | UserNode;
let flatCompanyData: NodeUnion[];

// ...

const node = flatCompanyData[0];
if (node.type === NodeType.dept) {
  // x is correctly inferred as DeptNode in here
}

对于这种类型,编译器知道,数组中的元素可能具有类型 NodeType.dept 的唯一方法是它是否是 DeptNode,因此可以正确推断类型。

这还留给我们第二个问题。即使您将此联合类型用于 flatCompanyData,您的代码仍然无法运行。原因是 Array.filter 函数。对于类型 T[] 的数组,它总是 return 类型 T[] 的数组,无论你在里面检查什么。

但幸运的是,typescript 为此提供了一个方便的解决方案:自定义类型保护。定义函数(包括匿名函数)时,您可以使用类型谓词作为其 return 类型来告诉编译器此函数充当类型保护。

function isDeptNode(node: NodeUnion): node is DeptNode {
  return node.type === NodeType.dept;
}

现在你可以在过滤器中使用这个函数让打字稿意识到你在做什么。

const selectDept: DeptNode[] = flatCompanyData.filter(isDeptNode);

也可以将类型谓词指定为匿名函数的 return 类型,而无需先定义 isDeptNode 函数。

const selectDept: DeptNode[] = flatCompanyData.filter(
  (x): x is DeptNode => x.type === NodeType.dept
);