Typescript without Typescript
Today, I wanted to talk about how I use Typescript without actually using Typescript (and why I do that even though I hate Typescript). Let’s dig in!
JSDoc and vanilla JS
With Kelp UI, I decided that some type checking might be useful, but I really didn’t want any .ts files in my project.
Keeping everything vanilla CSS and JS was an important goal. It’s the same reason I completely ditched Sass in favor of vanilla CSS.
Typescript actually understands JSDoc.
That means you can get a majority of the benefits of Typescript without having to actually use Typescript of learn it’s weird bastardized version of JavaScript.
Why even bother?
While I do still think the challenges of type safety are generally overstated, I have seen Typescript occasionally catch errors I would have missed.
For example, when working on a big project, I might try passing a string into a function when it actually expects a boolean. Or maybe I try to pass in a NodeList to a function that expects an Array (and therefore has access to different methods).
Or maybe you try to run a method that exists on the type of content you expect to have, but the way your code is written, that might not actually be the type of data you have.
Typescript will yell at you, and then you can fix it.
I do still find that it more often catches errors that aren’t: things that you know won’t be issues based on how the code is written, but Typescript believes with it’s whole heart are problems.
But, for me, it catches just enough issues I would have missed that I’m finding it worth it to use now.
And as a bonus, my text editor surfaces all sorts of useful information when you hover over things that makes writing code faster and easier.
How to do Typescript checks with JSDoc
You can document functions just like you normally would with JSDoc…
/**
* Add two numbers together
* @param {Number} num1
* @param {Number} num2
*/
function add (num1, num2 = 0) {
return num1 + num2;
}
To document types inline, use /** @type {} */ (must start with /**)…
/** @type {NodeList} */
const tabs = document.querySelectorAll('.tabs');
If a variable could be multiple types, you can list them all by separating them with a pipe (|)…
/** @type {HTMLFormElement | null} */
const signinForm = document.querySelector('#sign-in');
In Typescript, you would occasionally need to tell Typescript, “Yo, this thing is this type, despite what you think.” (Often called typecasting).
In Typescript, you do with with angle brackets before the item (<Type>)…
// In Typescript...
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');
function getNextLevel (index: Number) : String {
const nextHeading = <Element>headings[index + 1];
return nextHeading?.tagName.slice(1) || null;
}In JSDoc, you again use /** {Type} */, and wrap the item in parentheses (())…
// In JavaScript
/** @type {NodeList} */
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');
/**
* Get the level of the next heading
* @param {Number} index The index of the current heading in the `headings` NodeList
* @return {String} The heading level
*/
function getNextLevel(index) {
const nextHeading = /** @type {Element} */ (headings[index + 1]);
return nextHeading?.tagName.slice(1) || null;
}
One of the biggest gotchas I find with Typescript is when it insists that an event object might not have the property your want, because it doesn’t know what type of element is triggering the callback function to run.
function handleChangeEvent(event) {
// Typescript gets made, because not all events have a target,
// and not all HTMLElement's have the checked property
const isChecked = event.target.checked;
}
You can fix this by checking for instanceof on the event, the event.target, or both.
function handleChangeEvent(event) {
if (!(event instanceof FocusEvent)) return;
if (!(event.target instanceof HTMLInputElement)) return;
const isChecked = event.target.checked;
}
If needed, you can also declare a @typedef to use for custom types. This is particularly useful when working with custom elements and web components, which often have custom public methods.
For example, here’s how I document Kelp’s <kelp-toast> element’s public .notify() method, which is used in another script.
/**
* @typedef {Object} Toast
* @method notify Create a toast notification
*/
/** @type {Toast | null} */
const toastEl = document.querySelector('kelp-toast');
toastEl.notify('This is a toast notification message!');
Text Editor Integrations
VS Code includes Typescript integration out-of-the-box.
If you use Sublime Text, I recommend installing LSP and the LSP-typescript extension.
With either of these, you get argument hinting when writing functions, and can hover over functions and variables to see more details about them.
And, it will show inline error notifications, so you can catch and fix bugs in real time instead of after-the-fact.
Configuring Typescript
In order for any of this to work, you need to actually install Typescript into your project.
You also need a tsconfig.json file. Here’s mine…
{
"include": [
"src/js/**/*.js",
"types.d.ts"
],
"compilerOptions": {
"target": "es2022",
"allowJs": true,
"checkJs": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"strictPropertyInitialization": false,
"noImplicitAny": false,
"skipLibCheck": true,
"noEmit": true
}
}Adjust the include array to point to the files or directories you want type checked.
The types.d.ts file is where you can add custom universal type definitions. I have one for the KelpWCInstance class I use for all Kelp’s web components, which all include an init() function.
interface KelpWCInstance extends HTMLElement {
init: () => void;
}And if you ever want to actually run Typescript against your files rather than just using it for text editor hints, run npx tsc in your Terminal window.