Some nice TypeScript
I wanted to share a snippet of a TypeScript function I wrote recently. Although it’s not a particularly interesting scenario, I like it because it is simple and elegant, if I say so myself.
At the moment I am working on an AWS Lambda that queries the API of the Toggl time-tracking service to retrieve a list of time entries for a given date range. The API returns an array of time entries with the following structure:
{
"id": 3136598124,
"workspace_id": 2360906,
"project_id": 193325937,
"task_id": null,
"billable": false,
"start": "2023-09-21T20:10:02+00:00",
"stop": "2023-09-21T20:39:54Z",
"duration": 1792,
"description": "pytest blog post",
"tags": [],
"tag_ids": [],
"duronly": true,
"at": "2023-09-21T20:39:54+00:00",
"server_deleted_at": null,
"user_id": 3700888,
"uid": 3700888,
"wid": 2360906,
"pid": 193325937
}
Notice that the project_id is a number rather than the human-readable string
that appears in the Toggl UI. When I return the data to the frontend I want to
have the name of the project rather than this number. So I wrote the following
transformer:
interface IProject {
id: number
name: string
[key: string]: unknown
}
type TProjectMap = Record<IProject["id"], IProject["name"]>
const getProjects = async (): Promise<TProjectMap> => {
const workspace = process.env.TOGGL_WORKSPACE_ID
const togglClient = new TogglClient()
const projects: IProject[] = await togglClient.get(
`workspaces/${workspace}/projects`
)
return parseProjects(projects)
}
const parseProjects = (projects: IProject[]): TProjectMap => {
return projects.reduce((projectMap: TProjectMap, project: IProject) => {
projectMap[project.id] = project.name
return projectMap
}, {})
}
getProjects obviously calls the Toggl API to retrieve the list of projects for
the workspace. The parseProjects function takes the array of projects and
returns an object with the project IDs as keys and the project names as values.
In my main controller function, I will then be able to transform the project IDs
like so:
const projects = await getProjects()
const projectName = projects[193325937]
console.log(projectName)
// "Practical study"
What I like:
-
The time-entry returns several properties that I am not interested in - I only want
idandname. I could manually type the other properties to ensure full type-safety but life is short and I can’t be bothered. One get-out would be to useanyfor the other properties but this is obviously self-defeating in TS. Instead I useunknownand then type-assert the return value ofparseProjectstoTProjectMapwhich is a record ofnumberkeys andstringvalues. This means that if I try to access a property of theprojectMapobject that is not a number, I will get a type error with the added benefit that I make it transparent to the reader that I am solely interested in theidandnamekeys. -
Rather than using a
forEachloop or amapfunction combined with afilter, I make good use ofreduce, reduce being a very semantic account of what I am seeking to do.
More generally, I feel like I have purposefully harnessed the explanatory value of the type system to make the code more readable and self-documenting. This is a good example of where TypeScript can reduce the need for boilerplate and comments, in addition to the obvious benefits of type-safety.