Scripting with VSCode Tasks and Typescript
Kirk Shillingford
Posted on November 16, 2022
Cover photo by Markus Winkler on Unsplash
This article will attempt to demonstrate the minimum setup required to run Typescript scripts through Node using VSCode Tasks. We're going to create from scratch a small project that allows someone to:
- Write some Typescript
- Run that Typescript without converting it to Javascript first
- Run the entire process using VSCode tasks to capture user arguments without using the command line.
The article assumes a passing familiarity with Visual Studio Code, Typescript, and Node.js, but I've tried to include links wherever someone might want additional context.
Important Note: This article is meant to be more of a how-to than a why-to, but I felt like it was worth talking about the reason behind scripting in the general, and Typescript in particular. If you'd rather jump straight to the implementation though, you can head straight there.
Why script in Typescript? Why script at all?
Recently, I've been taking a hard look at my work patterns, and making note of opportunities for improvement. What I've discovered are a large amount of repetitive tasks that are made unnecessarily inconsistent and haphazard because I do them from scratch whenever I need them, with little preparation.
I don't think all tasks require automation, and there's a real measure of "value-gained-per-time-lost" that should always be considered, but at least for me personally, it feels like a space where I can make meaningful changes to great benefit.
Scripting can be a great way to reduce cognitive overhead and save time by automating away actions that are:
- Repetitive, meaning the same actions take place with sufficient frequency and duration that there's a significant chunk of your time repeating the same actions.
- Tediously Mechanical, meaning actions that require specificity and precision, but not analysis (long sequences of commands, file creation and naming, migration of text, etc)
Often we associate scripts with the built-in environments that interface with our operating systems, like bash or Powershell, but we can use any language for scripting provided it has some mechanism (like a runtime environment for executing instructions on a server/OS.
Frequently in my experience, scripts have been described and treated as short, throwaway code, or tedious config work, devoid of the care and rigour we apply to application or operations code.
But if something is important enough to commit to code, it's probably important enough to try to write correctly. The paradigms of readability, testability, and extensibility are just as relevant for short, targeted actions as long-standing operations. Maybe even more so?
Enter Typescript
Anyone who knows me knows I have a deep fondness for Type Driven Development and a particular fascination with Typescript's Type system. Many folks do not see the value in static type systems and I don't see the value in refuting those opinions; I can only attempt to express my own satisfaction through the work I do. I try as much as possible to write code with the support of a strong type system whenever I am able to, and for me, the benefits are real and worthwhile. So this is for anyone who might be interested in the same.
Also, just as a personal note, I love writing scripts. I love writing short, powerful, durable piece of code, that do a thing, well. I like the completeness of small actions, done well. Scripts make me happy.
Manually debugging scripts though, does not make me happy. It does not spark joy. There is a wailing and gnashing of teeth. So I want every possible safeguard to ensure my scripts work as well as they can the first time around. I'm not under the delusion that I can prevent all errors before I run the code. I'd just like to prevent some of them.
Implementing Our Scripts
We're going to split this into two major parts; firstly, how to run typescript code in node without prior compilation to js, and then how to configure that process using a Visual Studio Code Tasks features.
First step is ensuring we have both Node and Visual Studio Code on your machine. I'm going to create a new project folder and open it up in VSCode. We won't need VSCode till we implement tasks though, so working in a File Explorer or Terminal is also fine.
In our project folder, we're going to create our first file, createTemplate.ts
that contains the code we want to execute.
import { writeFile } from 'fs'
const [_, __, cwdInput, dateInput] = process.argv
const rawDay = new Date(dateInput)
const day = new Date(rawDay.getTime() + rawDay.getTimezoneOffset()*60000);
const path = `${cwd}/${day.toLocaleDateString('en-CA')}.md`
const template =
`
${day.toLocaleDateString('en-CA')} - ${day.toLocaleDateString(undefined, { weekday: 'long' })}
# Gratitude
-
-
-
# Frogs
-
-
-
# Wins
-
-
-
`
console.log(path, template)
// writeFile(path, template, console.log)
This script creates a markdown file with some prewritten headers that I tend to use when I write my daily journals. It then sticks that file in the current working directory, with some context of the current date. Currently, we're just opting to log it console first though, while we're getting setup.
As it stands, it currently works fine, but we have two problems.
First, we're getting some rudimentary, and dubious type hints right now. We're being told that the command line arguments cwdInput
and dateInput
are strings, but there's actually no guarantee that they're there at all. process.env
pulls info from the environment the script is run in, and thus is an I/O operation, which is inherently impure. Typescript has the capacity to warn us about this, but isn't currently configured to surface that, so let's give it that capacity.
We can add a tsconfig.json
file that informs VSCode's typescript language server of how we want it to compile our code, and what guarantees we'd like.
{
"compilerOptions": {
"target": "ES2021",
"strict": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "NodeNext",
"lib": [
"ES2021"
],
"noUncheckedIndexedAccess": true,
}
}
We've got a few things in there, but most important is the strict: true
and noUncheckedIndexedAccess: true
, configuration. This makes the compiler much less permissive about unsafe operations and our intellisence now correctly sees that our process values are string | undefined
.
If you've been following along directly, your project folder should now look like,
But how to run it
The second problem we've got with this script right now is of course, we've got nothing to run it! Because Node can't directly run Typescript!
Note: To be clear, there is currently no popular runtime engine for Typescript. Meaning, there is no application that accepts Typescript natively, turns it to an AST, and executes in an environment, without first, compiling it to javascript. That's what Typescript is really:
- An expanded syntax and type system that layers over Javascript syntax, providing an powerful set of additional information, context, and guarantees at compile time
- A compiler than converts that to javascript to be consumed by some runtime.
- A language itself for expressing complex shapes and a powerful engine for type manipulation
Typically, we would compile our Typescript to Javascript, and run those javascript files with Node. But in this case, we don't need to ever use those javascript files. And this code isn't going to the browser either. So we have the option of finding solutions that don't need those intermediate javascript files.
Ts-Node
The one we're going to use is ts-node. Ts-node has a simple premise; you can pass it typescript files, and it will convert them to JS, and pass them to node for you. It can function also function as a REPL.
To get it, we first add a package.json
to our directory. A lot of folks think you need a filled out package.json for npm to function effectively. But really you just need the file.
We can then install the necessary packages using the following:
npm i -D typescript ts-node
That will install ts-node and the Typescript compiler locally, as dev-dependencies. There's an argument for having these as just regular dependencies here, but we're not planning to ship this code anywhere else, so I'm not sure what the best practise is (if anyone reading has opinions, please let me know!)
So we should now see something like this in out package.json
.
{
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
}
}
Check in time for those following along! Now your project folder should look like this
We've added a little addition to our tsconfig
to tell ts-node
to not run type-checking on our code, because the language intellisense in VSCode is already doing that for us. We've also added some controls to ensure Typescript doesn't attempt to output any javascript files.
{
"compilerOptions": {
"target": "ES2021",
"strict": true,
"noUnusedLocals": true,
"noEmit": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "NodeNext",
"lib": [
"ES2021"
],
"noUncheckedIndexedAccess": true,
"allowJs": false
},
"ts-node": {
"transpileOnly": true,
}
}
And now we can simply tell ts-node to run our typescript using npx
npx ts-node createTemplate.ts /path/to/root/ 2022-11-15
And we get the result we want!
Yay, that worked! And honestly, we could leave it there if you don't mind typing commands in the terminal. We don't need vscode for any of this really, we could let ts-node do our type-checking.
But we don't want to do our own command line instructions now do we? We want to AUTOMATE. And particularly, I want VSCode to do the work for me!
Part 2 - Tasks
To start with, I'm going to pull a line of text straight from the VSCode documentation:
"Tasks in VS Code can be configured to run scripts and start processes so that many of these existing tools can be used from within VS Code without having to enter a command line or write new code."
Great, that looks like exactly what we want.
"Workspace or folder specific tasks are configured from the tasks.json file in the .vscode folder for a workspace."
Say less, fam.
Jokes aside, this is exactly what we're going to do next, making a .vscode
folder in our project and adding a tasks.json
file.
The tasks API is pretty robust, coming with a suite of built-in variables, integrations for common operations like build-time and run-time actions, and pre-existing hooks and plug-ins, but for our needs we just need a small custom task ourselves.
Here is the json that we will add to out tasks.json file to tell VSCode how to execute our little ts-node script.
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "CreateDay",
"command": "npx",
"args": [
"ts-node",
"./createBujoTemplate.ts",
"${cwd}",
"${input:date}"
]
}
],
"inputs": [
{
"type": "promptString",
"id": "date",
"description": "Date for your new template. Preferred format yyyy-mm-dd",
"default": ""
}
]
}
I'm going to try my best to step through the various parts of this json.
- First, we declare the task version we're working with. At the time of writing, 2.0.0 is the default.
- Then we make a
tasks
field; a list of the all the tasks we would like VSCode to use. We currently have the one. - In that task, we specify the task type (shell or process), the tasks unique label (it will yell if you give two tasks the same label), and a way to specify the main command, and any arguments it needs.
- Note "${cwd}" as our third argument. CWD (which stands for current working directory) is a built in variable VSCode makes available for use in writing tasks.
- Below that, we define another variable
"${input:date}"
, which makes use of VSCode's ability to define custom input variables that need to be provided by the user. - After our task list, we define our list of inputs, where we specify an input we call date.
- The type
promptString
is a VSCode definition that just means the user needs to provide some string of text. There's options for providing a list or dropdown or even other commands. We're just using a string. - Finally, we add a description and a default value for our input.
Putting it all together
And with that all in place, we should be be hit Cmd-Shift-P
(or Cntl-Shift-P
on Windows Systems) to bring up the Command Palette and find the Run Task
command.
That will open up a dropdown showing available tasks where we find our newly added custom task CreateTemplate
.
Selecting that takes us to a prompt asking if we need any specific debugging environment while running the script; we can just click continue without scanning output
for now.
Finally, we get a prompt asking for that input we defined earlier!
With all that in place, we can watch as the terminal details the running of our code.
And if we check the file system...
We did it!
To Sum Up
So just to recap, we originally had a very manual process, that required us to create and populate a file with the same information every day. And now we've reduced all those to a few keystrokes! I know this doesn't feel like a lot, in terms of the complexity of the code we automated away, but we've barely scratched the surface of what tasks are capable of here. We could've set this task to run whenever we opened our project, or connected it to a cron job or a Github action.
Not everything should be automated, but it's worth considering what could be automated, and if it's your goal to get this kind of behaviour, hopefully this article let you realise it's not as far away as you might think. Give it a try.
Build your own tools sometimes, not because you have to, but because you want to.
Thank you for your time.
Posted on November 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.