There’s no denying that TypeScript has taken hold in the JavaScript community. And there’s no wonder when it offers features like improved Intellisense, static analysis (aka “type-checking”), and inline documentation.
These features are not unique to TypeScript. They’re part of any strongly-typed language, and they translate to improvements in productivity and code-quality such as:
- Write code faster with autocomplete code-suggestions as you type.
- Warns you if you have a typo or error in your code.
- Easier to introduce new people into the code base.
- Better collaboration for team members across code they did not write.
- Can prevent broken code getting through automatic deployments.
- Makes maintaining old code is easier and safer to modify.
- Can be used to automatically document your project.
That’s all well and good, but I’m not trying to sell you on using TypeScript. This article is intended for JavaScript developers that have heard of TypeScript and are interested in getting stared with it, or anyone that has already tried TypeScript and want to give it another perspective.
Unlike many tutorials today, this one will focus on minimizing the technical requirements to follow along. We will get up and running with TypeScript without opening a terminal, installing dependencies, or even initializing an NPM project. In the advanced portion we will need to do a bit of that, but for most users, all you will need is Visual Studio Code as your editor.
Level 1: Enable TypeScript in JavaScript Files
VS Code has taken the tech world by storm. If you’re not familiar, it’s a code editor, but I would wager that if you are writing JavaScript regularly, you probably already use it.
Did you know that VS Code has TypeScript built-in? That’s how it’s able to provide some basic intellisense and auto-suggest.
For example, we cane create an object with a property called hello
and give it the value "world"
. When we try to access properties of that object, VS Code will auto-suggest hello
for us. Not only that, but it will also tell us that the property hello
is a string
.
This is very basic type checking and it can be quite helpful. However, there are several errors that can still make it into our codebase that could have been avoided if we had enabled better type-checking.
For example, if we accidentally passed a number to a function that was expecting a string, we might run into issues.
VS Code actually has a feature that enables full-on type checking for JavaScript files. And the best part is it’s super easy. All you have to do is add this comment to the top of the JavaScript file you want to add type checking to:
// @ts-check
With that comment in place, we’ve enabled stricter type-checking in our file. Now we will see more hints if we introduce possible errors into our codebase. For example, if we try to overwrite the object’s hello
property with a number, we will get a warning telling us “Type ‘number’ is not assignable to type ‘string'”:
At this point, our previous function will not give us any hint that our code has an error, and that is because TypeScript does not know that the input can only be a string. To fix that, we can add types to our JavaScript with JSDoc.
JSDoc is a system of adding contextual documentation to your source code by using comments, and it can be used to generate documentation websites automatically. The benefit that we are most interested in today, however, is that TypeScript has support for parsing JSDoc comments.
For our example function, we can tell TypeScript that the first parameter to the yell
function is a called str
and is a type of ‘string’. The result of the function is also a ‘string’.
Now when we try to pass a number to the function, we will see a red squiggly warning. And hovering over it, we will see the warning, “Argument of type ‘number’ is not assignable to parameter of type ‘string’.” Thanks!
To learn how to document everything with JSDoc takes time, study, and practice. It’s a bit beyond the scope of today’s subject, but a great place to learn more is jsdoc.app.
Level 2: Enable TypeScript in JavaScript Projects
If you’re like me, you will quickly see the benefits of using JSDoc to document your code while also having TypeScript yelling at you when you messed up. But if you work on large JavaScript projects, it won’t take long before you get tired of adding // @ts-check
to every single file.
Fortunately, VS Code offers some ways to let it know that you would like it to do that TypeScript thingy on every JavaScript file, please. One way is by setting the “Check JS” configuration to true
. In the settings.json
file the setting is "javascript.implicitProjectConfig.checkJs": true
You can do this on a user or workspace level, but that will only be enabled for you.
If you are working on a team and would like to enable this feature on a project level for everyone, you can do so by adding a tsconfig.json
file to your project’s root folder. Usually, I’ll just copy the same file to each project, but let’s create a brand new file from scratch with the TypeScript initialization command:
This file can be used to configure a few options, but for now we are only interested in the one that tells VS Code to use TypeScript to check JavaScript files. For more details about tsconfig.json configuration options, see www.staging-typescript.org/tsconfig.
{
"compilerOptions": {
"checkJs": true, /* Report errors in .js files. */
}
}
I prefer doing things this way because I lead a team and I don’t want to explain to folks how to enable type checking in their settings. And we don’t need to no worry about keeping everyone’s editor settings in sync.
Now, whichever way you prefer, you will have TypeScript checking all your JavaScript files for potential bugs without you needing to do anything else. Hooray!
JSDocs has support for a number of built-in types: string
, number
, boolean
, array
, promise
, function
, etc. However, it doesn’t take long before you want to create type definitions beyond the basic primitive values. For example, let’s say we want to define a “Dog” object type that has a “breed”, an “age”, and an optional “name” property. JSDoc can still support you there.
We can define the type like this:
/**
* @typedef {object} Dog
* @property {string} breed
* @property {number} age
* @property {string} [name]
*/
There are a couple of different ways to define objects, this is one syntax. I don’t want to get too off-topic by spending time on specifics around defining types. For more details on that, feel free to explore the JSDoc documentation. It’s also worth looking into TypeScript’s Generics and Utility Types as you get into more advanced needs.
My point here is to show you how to import type definitions across your codebase. This took me a bit of time to learn, so hopefully, I can save you some searching.
Let’s say we wanted to have one file to define some global types in. Let’s call it types.js
. We can put our Dog type definition in it and from a different file, we can import and use that type in another file by referencing it’s relative path:
/** @type {import('./types).Dog} */
const myDog = {
breed: 'Chiweenie',
age: 4,
name: 'Nugget'
}
If we find ourselves using the Dog
type in many places in the same file, we can save ourselves some typing by redefining the type definition locally:
/** @typedef {import('./types).Dog} Dog */
/** @type {Dog} */
const myDog = {
breed: 'Chiweenie',
age: 4,
name: 'Nugget'
}
If you’ve been trying this out while reading this article, you may have run into one small issue. As it is right now, we can’t import anything from our types.js
file because that file is not a JavaScript module. Our editor will tell us so, “File ‘/path/to/types.js’ is not a module.”
The solution is add an export to that file. You can use either CommonJS or ES Modules syntax. The exported value doesn’t matter. It can even be undefined
. For example, any of these lines would do (note that you only need one):
// Works
module.exports = {}
// Sure
exports.merp = ''
// Why not?
export default = null
// Go for it
export const thingamabob = undefined
It’s also possible to import type definitions from a third party library. The syntax is very similar, but instead of using a relative path, you just reference the library by name. For example, a Vue.js component can be typed with:
/** @type {import('vue').Component} */
Not all libraries provide type definitions. You just have to try and see if VS Code auto suggests any for you to use. But in the case your library does not provide type definitions, there may still be a community provided types package at definitelytyped.org, so give that a look too. VS Code has a feature called “Automatic Type Acquisition” which will automatically look for and install community type definitions for you.
Lastly, I’ll point out that you can also write your type definitions in a TypeScript file if you prefer that syntax. The import will look exactly the same for JSDoc. The only thing that changes is the file extension (ends with .ts) and the syntax. For example, if we wanted to define our global types above with TypeScript, we can change the file name to “type.ts” and the contents like so:
export interface Dog {
breed: string
age: number
name?: string
}
Level 3: Integrating TypeScript into CI/CD Pipeline
Up until now we’ve accomplished all of the above without a build step, without installing any dependency (besides our editor), without initializing an NPM command, and even without opening up the command line. How refreshing is that!?!? It’s just plain old JavaScript, but with super powers.
Now we’ll start to venture into more complex questions. Can we prevent our code from deploying if a bug is introduced into the code?
The rest of this section will make the following assumptions:
- You are comfortable working with the command line.
- You have some experience with NPM (if not, you can read NPM’s getting started page at docs.npmjs.com/getting-started).
- You are familiar with CI/CD (Continuous Integration/Continuous Delivery) concepts.
- You already have an NPM project initialized with a package.json file.
Our goal is to run the TypeScript compiler from within a CI/CD environment so that the system knows whether or not our code has type errors. This means we can no longer rely solely on VS Code to provide TypeScript for us, and to tell us if our code has errors. We will need to provide the CI/CD environment with a version of TypeScript and a script to run.
The first thing we will do is run this command in the terminal (from the same folder as our project):
npm install --save-dev typescript
This will install TypeScript locally, and it will also update the package.json file by including the "typecript"
packages as a dev dependency. This will be necessary for any other computer running our project to know what dependencies to install. With that in place, TypeScript is now available to the project without relying on VS Code.
Next, we need to be able to tell the computer how to actually run TypeScript. We can update the NPM “scripts” section of our package.json file with the following line:
"ts": "tsc"
This will add a new scripts called “ts” that will run the “tsc” command which is the TyepScript compiler. Now we can run the command “npm run ts
“, but right now we have two errors.
- TypeScript wants to know the path to the files to run on.
- TypeScript only works with .ts files, and all of our files are .js files.
Here is where you will need to make a decision for yourself. Do you want to keep writing JavaScript files, or do you want to write TypeScript files?
Personally, I think keeping everything in JavaScript makes life simpler. The TypeScript compiler supports JavaScript files fine, it’s just not enabled by default. So for the rest of this tutorial, we will do things in JavaScript, but keep it in mind that this is my personal preference (more on why at the end).
To fix these issues, we need to explicitly tell TypeScript which files to check and we need to use the allowJs
configuration to allow it to run on JavaScript files. Assuming our JavaScript was written in a file at ./src/index.js
we have a few options.
- We could add “
--allowJs ./src/index.js
” to our NPM script in the package.json file - We could add the above command every time we call the NPM script:
npm run ts -- --allowJs ./src/index.js
- Or we could use a tsfonfig.json file in the root of our project.
Since we already have a tsconfig.json file, let’s use that. We’ll need to define the "files"
array, and set "allowJs"
and "noEmit"
to true
:
{
"files": ["./src/index.js"],
"compilerOptions": {
"checkJs": true, /* Report errors in .js files. */
"allowJs": true, /* Allow parsing javascript. */
"noEmit": true, /* Do not emit outputs. */
}
}
We set the "noEmit"
configuration to true
here because TypeScript is normally used to transpile code. Meaning it takes in some code and transforms it in some way. Normally this is to take in a TypeScript file and return a JavaScript file, but since our files are already JavaScript it would result in overwriting our files. We’ll look at doing this in the next section, but for now let’s just keep this simple.
If we run our “npm run ts
” command now, we shouldn’t have any configuration errors. We should only see errors related to bugs in our code. To use one of our previous examples, trying to overwrite a property in an object that was defined as a string will yield an error.
Great! Now we have everything in place to integrate this sort of type-checking into our automated deployment process. All we need to do is make sure the deployment process calls our "npm run ts"
command, and as long as there are no errors, it will continue on it’s way.
Unfortunately, there are too many variables for me to tell you exactly how to integrate this into your own deployment process. It will be slightly different for everyone, but if you’ve made it this far then I think it’s fair to hope you can take it from here.
There is just one last thing I want to mention. TypeScript is a great addition to a testing suite, but it is in no way a replacement for automated tests. TypeScript can remove several different kinds of bugs from getting into the code base, but if your project relies on automated deployments, you should also use unit or integration tests.
As an example, TypeScript may prevent you from using a string in a place that must be a number, but I don’t think it could prevent you from using a negative number where only a positive number is allowed.
All that is to say that the I would recommend implementing both static analysis and automated tests into your system. My favorite tool for testing JavaScript projects is Jest.
Level 4: Generating Type Definitions for Open-Source Libraries
As a JavaScript developer and a maintainer for an open-source Vue.js library, I feel comfortable saying that the expectations for open-source libraries are quite high. One growing expectation is type definitions (sometimes referred to as “TypeScript support”) either within the library or through community projects like definitelytyped.org.
The good news is we can take our current setup and without much effort, tell TypeScript to create type definition files for our project. Once we are done, we will be able to publish our library and users will have beautiful type definitions to help improve their experience interfacing with our library.
To get started, we will need to make a few more modifications to our tsconfig.json file. We’ll want to remove the "noEmit"
setting (or set it to false
), set "declaration"
and “emitDeclarationOnly” to true
, and provide a path for the "outDir"
.
The new file should look more or less like this:
{
"files": ["./src/index.js"],
"compilerOptions": {
"checkJs": true, /* Report errors in .js files. */
"allowJs": true, /* Allow parsing javascript. */
"declaration": true, /* Generates '.d.ts' file. */
"emitDeclarationOnly": true, /* Only generate '.d.ts'. No JS */
"outDir": "./dist", /* Send output to this directory. */
}
}
You can choose whatever path you want for the "outDir"
, but that is a required setting. That is where our generated type definition files will exist. I set "emitDeclarationOnly"
to true
because we are already working with JavaScript so there is no need for a compilation step. Whenever I need a build step, I usually use Babel.js and Rollup.js, but we’re keeping things simple today.
Now that our type definition files are being generated and sent to a /dist
folder, we are just about done. The last step is to tell NPM those files exist so any developer consuming our library can benefit. We’ll need to modify our package.json file.
To publish anything on NPM, the "name"
and "version"
properties are required. We can also define the "types"
(aka "typings"
) property to tell TypeScript which folder to look in for our library’s type definition files. Apparently, this is not required if your type definition files (ending in .d.ts
) live in the same folder as your code. Still, it’s recommended to explicitly declare where your type definitions will exist, and since we set our "outDir"
above to the /dist
folder, that is what we will use.
Now our package.json file may look something like this:
{
"name": "nuggetisthebest",
"version": "1.0.0",
"types": "dist",
"scripts": {
"ts": "tsc"
},
"devDependencies": {
"typescript": "^4.1.3"
}
}
For more information about publishing libraries to NPM, see the NPM docs.
Closing Thoughts
That was quite a journey. I hope it was thorough enough to learn something without being overwhelming. It outlines, more or less, my own personal journey with learning and using TypeScript. So it’s not entirely free of personal opinions and preferences. Your experience and preferences may vary.
It’s also worth mentioning that although I prefer using TypeScript through JSDocs, it’s more common to use .ts
files. TypeScript can parse JSDocs without issue for the most part, but it’s not a one-to-one relationships. There are some slight differences which are outlined in further details in the TypeScript docs.
I prefer the JSDocs approach for these reasons:
- There’s no need for build steps. It’s just plain JavaScript.
- Which means I can copy and paste code to any JavaScript project.
- No new syntax so it feels easier (for me) to learn.
- Less noise mixed into my code.
Hopefully you learned something new today and I answered any questions that came up while reading this article. If not, please reach out to me on Twitter so I can answer them for you and anyone else that might have the same questions.
If you enjoyed this article, please let me know by sharing it on Twitter. You can also follow me or sign up for my newsletter if you’d like to know when a new article is published.
Resources
- Working with JavaScript by VS Code
- JSDoc Cheatsheet by Devhints
- Type Safe JavaScript with JSDoc by Robert Biggs
- JavaScript Type Linting by Robert Biggs
Top comments (0)