There is organizational data with a hierarchical structure.
Organizational nodes are divided into Root, Dept, and User.
The object property type is assigned a type suitable for node data.
So, I hope that the node interface suitable for the object type will be attached conditionally.
To solve this problem, I tried Index Types and Conditional Types, but they were not successful.
Any help would be appreciated🙇‍♂️
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: "[email protected]",
},
{
nodeId: "0.0.1",
type: NodeType.user,
title: "User 2",
employeeNumber: "201912",
userPrincipalName: "[email protected]",
},
],
},
{
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,
});
});
There's two problems in your code:
First of all, an element of type Node
is not necessarily of type DeptNode
just because it has NodeType.dept
. Consider the following object:
{
nodeId: '0',
type: NodeType.dept,
title: 'Company 1'
}
This is a completely valid Node
but not a valid DeptNode
as it lacks the fields deptId
and companyCode
. So the compiler is in fact right when it does not treat it as a DeptNode
. But there is a solution to this. Instead of using an array of Node
s you can define a union type of all possible node types and use it as type for flatCompanyData
like this:
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
}
With this type the compiler knows, the only way an element in the array could possibly have the type NodeType.dept
is if it is a DeptNode
and so the type is inferred correctly.
This still leaves us with the second problem. Even if you use this union type for flatCompanyData
, your code will still not work. The reason is the Array.filter
function. For an array of type T[]
it will always return an array of type T[]
, no matter what you check inside.
But fortunately typescript comes with a handy solution for this: custom type guards. When defining a function (including an anonymous function) you can use a type predicate as its return type to tell the compiler that this function acts as a type guard.
function isDeptNode(node: NodeUnion): node is DeptNode {
return node.type === NodeType.dept;
}
And now you can use this function inside the filter to make typescript realize what you are doing.
const selectDept: DeptNode[] = flatCompanyData.filter(isDeptNode);
It is also possible to specify a type predicate as the return type of an anonymous function without defining the isDeptNode
function first.
const selectDept: DeptNode[] = flatCompanyData.filter(
(x): x is DeptNode => x.type === NodeType.dept
);