Issue
I want to create a nested object, which represents a hierarchy more or less, based off a file path from an object in an array, while also including a relevant ID for that file, in Typescript.
I have an array of objects like this:
interface FileInfo {
id: string;
path: string;
}
let files: FileInfo[] = [
{id: '1', path: '/root/library/Folder 1/Document.docx'},
{id: '2', path: '/root/library/Folder 1/Document 2.docx'},
{id: '3', path: '/root/library/Folder 2/Document 3.docx'},
{id: '4', path: '/root/library/Document 4.docx'}
];
Then when it is converted the desired type is:
interface TreeView{
id: string;
name: string;
children?: TreeView[];
}
id
:- If it’s a folder (not last element in the path), the
id
should be something like${name}_{uuid()}
. - If it’s the file (final element in path), then it should take the
FileInfo.id
value.
- If it’s a folder (not last element in the path), the
name
: The current element of the path, e.g.root
,library
etc
So the final output would look something like:
[
{
"id":"root_GUID",
"name":"root",
"children":[
{
"id":"library_GUID",
"name":"library",
"children":[
{
"id":"Folder 1_GUID",
"name":"Folder 1",
"children":[
{
"id":"1",
"name":"Document.docx",
"children":[
]
},
{
"id":"2",
"name":"Document 2.docx",
"children":[
]
}
]
},
{
"id":"Folder 2_GUID",
"name":"Folder 2",
"children":[
{
"id":"3",
"name":"Document 3.docx",
"children":[
]
}
]
},
{
"id":"4",
"name":"Document 4.docx",
"children":[
]
}
]
}
]
}
]
Similar questions
I’ve been heavily inspired by other SO posts like Create nested object from multiple string paths and Create nested object of file paths.
The solution outlined in this answer from the first SO thread linked has been helpful but am stuck making it work with an object, not an array of paths and also for it to be in Typescript.
Solution
I’ll show you a solution that works, and you can use it if you want.
First, one important thing (in my opinion) is to make use of recursive functions, since you’re basically going recursively through your path and appending to the global tree.
interface FileInfo {
id: string;
path: string;
}
let files: FileInfo[] = [
{id: '1', path: '/root/library/Folder 1/Document.docx'},
{id: '2', path: '/root/library/Folder 1/Document 2.docx'},
{id: '3', path: '/root/library/Folder 2/Document 3.docx'},
{id: '4', path: '/root/library/Document 4.docx'}
];
interface TreeView{
id: string;
name: string;
children?: TreeView[];
}
// The main function we call, to convert a list of fileInfo to the tree
function convertPathToTreeView(fileInfos: FileInfo[]): TreeView {
// We assume the tree starts at root, it would become very complicated otherwise because we would have no way of knowing the current depth of a path.
let tree: TreeView = {id: 'root_uuid_here', name: 'root', children: []}
// We go over each fileInfo and update the tree with the data contained in it.
fileInfos.forEach((fileInfo) => {
// We split the current path to get the file names, and we remove the leading / before splitting so we don't get '' as a first file name.
const fileNames = fileInfo.path.replace(/^\//, "").split('/')
tree = convertFolderOrFileToTree(tree, fileNames, fileInfo.id)
})
return tree
}
//This is a pretty classical recursive function, that will either return the whole tree, or append a new child to the tree by calling itself on a child of the tree.
function convertFolderOrFileToTree(currentTree: TreeView, fileNames: string[], fileId: string): TreeView {
// This is the base case: when no more files to parse, return.
if (!fileNames.length) {
return currentTree;
}
// If the current tree name is the same as the current file name, it means that the next file in fileNames will be a child of the currentTree. (for example, if currentTree.name === root and fileNames[0] === root, we know that we are at the root, and we can consider that the next fileName will be a child of the current tree, library in this case)
if (currentTree.name === fileNames[0]) {
return convertFolderOrFileToTree(currentTree, fileNames.slice(1), fileId)
} else {
// We check if current fileName is already a child of the tree.
const child = currentTree.children.find(t => t.name === fileNames[0]);
if (child) {
// If it is, we will parse the next fileName as the child of current fileName.
return {...currentTree, children: [...currentTree.children.filter(tree => tree.name !== child.name), convertFolderOrFileToTree(child, fileNames.slice(1),fileId)]}
} else if (fileNames.length > 1) {
// If the fileName is not registered as a child in the tree and it's not the last fileName (meaning it's a folder), we add it and parse the other fileNames recursively
const newTree: TreeView = {id: `${fileNames[0]}_uuid_here`, name: fileNames[0], children: []}
return {...currentTree, children: [...currentTree.children, convertFolderOrFileToTree(newTree, fileNames.slice(1), fileId)]}
} else {
// If the fileName is not registered as a child in the tree and it is the last fileName, meaning it's a file, we just add it to the children and return the tree.
const newTree: TreeView = {id: fileId, name: fileNames[0]}
return {...currentTree, children: [...currentTree.children, newTree]}
}
}
}
console.log(JSON.stringify(convertPathToTreeView(files), null, 4))
Don’t hesitate to ask if something is unclear, recursive functions can be a bit abstract.
If you want to allow for other roots than the /root folder, you can just specify an imaginary root, that I will call rootOfTheTree, so we can still have one tree, even if there are different roots.
You could also just have a function that returns several tree (for example if we have /root/file1 and /root2/file2), but it seems to me like that’s not what you need.
Here is the code, updated, and without any warning in typescript playground (the children property can be undefined and that caused typescript to not allow just using the property without checking)
interface FileInfo {
id: string;
path: string;
}
let files: FileInfo[] = [
{id: '1', path: '/root/library/Folder 1/Document.docx'},
{id: '2', path: '/root/library/Folder 1/Document 2.docx'},
{id: '3', path: '/root/library/Folder 2/Document 3.docx'},
{id: '4', path: '/root/library/Document 4.docx'}
];
interface TreeView{
id: string;
name: string;
children?: TreeView[];
}
// The main function we call, to convert a list of fileInfo to the tree
function convertPathToTreeView(fileInfos: FileInfo[]): TreeView {
// If we want to allow for different roots but still have one tree, we need to set a rootOfTheTree that is not a folder or a file, but just used to mark the beginning of the tree.
let tree: TreeView = {id: 'rootOfTheTree_uuid_here', name: 'rootOfTheTree', children: []}
// We go over each fileInfo and update the tree with the data contained in it.
fileInfos.forEach((fileInfo) => {
// We split the current path to get the file names, and we remove the leading / before splitting so we don't get '' as a first file name.
const fileNames = fileInfo.path.replace(/^\//, "").split('/')
tree = convertFolderOrFileToTree(tree, fileNames, fileInfo.id)
})
return tree
}
//This is a pretty classical recursive function, that will either return the whole tree, or append a new child to the tree by calling itself on a child of the tree.
function convertFolderOrFileToTree(currentTree: TreeView, fileNames: string[], fileId: string): TreeView {
// This is the base case: when no more files to parse, return.
if (!fileNames.length) {
return currentTree;
}
// If the current tree name is the same as the current file name, it means that the next file in fileNames will be a child of the currentTree. (for example, if currentTree.name === root and fileNames[0] === root, we know that we are at the root, and we can consider that the next fileName will be a child of the current tree, library in this case)
if (currentTree.name === fileNames[0]) {
return convertFolderOrFileToTree(currentTree, fileNames.slice(1), fileId)
} else {
// We check if current fileName is already a child of the tree.
const child = currentTree.children?.find(t => t.name === fileNames[0]);
if (child) {
// If it is, we will parse the next fileName as the child of current fileName.
return {...currentTree, children: [...(currentTree.children?.filter(tree => tree.name !== child.name) ?? []), convertFolderOrFileToTree(child, fileNames.slice(1),fileId)]}
} else if (fileNames.length > 1) {
// If the fileName is not registered as a child in the tree and it's not the last fileName (meaning it's a folder), we add it and parse the other fileNames recursively
const newTree: TreeView = {id: `${fileNames[0]}_uuid_here`, name: fileNames[0], children: []}
return {...currentTree, children: [...(currentTree.children ?? []), convertFolderOrFileToTree(newTree, fileNames.slice(1), fileId)]}
} else {
// If the fileName is not registered as a child in the tree and it is the last fileName, meaning it's a file, we just add it to the children and return the tree.
const newTree: TreeView = {id: fileId, name: fileNames[0]}
return {...currentTree, children: [...(currentTree.children ?? []), newTree]}
}
}
}
console.log(JSON.stringify(convertPathToTreeView(files), null, 4))
Answered By – jeremynac
Answer Checked By – Candace Johnson (BugsFixing Volunteer)