Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use custom type definitions in an ES6 project?

My team works on a relatively large NodeJS project, written in ES6, transpiled by babel, and then deployed as AWS lambdas with Serverless. This project is focused around consuming, mapping/transforming, and outputting one specific object type, which we have defined.

Our problem is, ECMA/JavaScript is not strongly typed, so if we make a mistake like treating a field as an array somewhere and a string somewhere else, there's nothing to catch that except runtime errors. We have also poorly documented the structure of this object, so sometimes consumers send us instances of the object with data in slightly misnamed fields that we say we process, but don't actually use.

I am looking for a way to create some kind of schema or type definition for this specific object in our project, so we can use that to correct our code, make our processing more robust, and create much better documentation for it. Now, I know VSCode offers some basic type checking in JavaScript, but I don't think it's feasible to try to JSDoc a really big object and then put that doc in every file that uses the object. I have found that VSCode can also, somehow, drive that checking with .d.ts files but I don't understand if or how I can leverage that for a specific, custom object that we've designed. Most of what I have found seems to be specifically related to pulling .d.ts files for external libraries.

So, TL:DR, Is it possible, in a NodeJS/ES6 project, to make one object, widely used throughout that project, strongly typed? Error checking in VSCode would be acceptable, but some kind of command-line linting that we could trigger before transpiling would be great too.

like image 726
cgm123 Avatar asked May 31 '17 13:05

cgm123


People also ask

Do you need to install type definitions for node?

The Node runtime does not ship with its own type definitions, so we need to import those types separately. Where can we find them? They are also in npm but need to be installed separately.

Why might you use type definitions for external libraries?

We do this so we get auto-complete and type checking for the explicit properties we're going to give it, while allowing us to still use any other property which we have not explicitly declared. This way we can look at the library's documentation and implement changes without having to update the type definition.

Where do I put TypeScript definitions?

TypeScript automatically finds type definitions under node_modules/@types , so there's no other step needed to get these types available in your program.


1 Answers

Alright, figured it out. After I posted this question, I kept googling and after an hour or so hit this article on StrongLoop by Sequoia McDowell: https://strongloop.com/strongblog/type-hinting-in-javascript/

I followed it pretty closely and, using the "typings" package, I was able to initialize a "typings" folder at the root of my project. The contents of that folder now look like this:

typings/
├── models/
│   └── myObject.d.ts
└── index.d.ts

The content of that index.d.ts file look like this:

/// <reference path="models/myObject.d.ts" />

And the contents of that myObject.d.ts file look something vaguely like this:

declare namespace MyProject {
  export interface MyObject {
    aString?: string;
    anotherString?: string;
    aComplexType?: ComplexType;
  }

  interface ComplexType {
    aString?: string;
    anotherComplexType: SecondComplexType;
  }

  interface SecondComplexType {
    aString?: string;
  }
}

With that done, I had to start marking instances of this object with JSDoc. That doc mainly took two forms:

/**
 * Creates an instance of UtilityThing.
 * @param {MyProject.MyObject} myObject
 *
 * @memberof UtilityThing
 */
constructor(myObject) {
  this.myObject = myObject;
}

/**
 * @param {MyProject.MyObject} myObject
 * @returns {MyProject.MyObject}
 */
function noOp(myObject) {
  return myObject;
}

and

/** @type {MyProject.MyObject} */
const myObject = containerObject.myObject;

With this setup, and the latest public release of VSCode, I was able to see errors in ES6 *.js files that I was currently editing that told me which attributes didn't exist, which were being assigned values of the wrong type, which were assumed to be the wrong type, etc.

Halfway there.

After some more research, I figured out that this isn't exactly a unique VSCode feature. It seems that they're using "tslint" or some customized version of it. Working with that knowledge, I added "tslint" to the project and worked out this npm script:

"tslint": "tslint --type-check --project tsconfig.json"

Here are the contents of the tsconfig.json that I landed on, though I'm not entirely sure all of these options are needed.

{
  "compilerOptions": {
    "target": "es6",
    "allowJs": true,
    "noResolve": true,
    "checkJs": true,
    "moduleResolution": "node",
    "types": [
      "node"
    ]
  },
  "exclude": [
    "node_modules",
    "coverage",
    "lib",
    "spec"
  ]
}

Running this "tslint" script, with that tsconfig.json, and the type definition files in the "typings" folder allows me to type check one specific object type throughout all files in the project with proper JSDoc. I did run into a small issue but that seems to have been fixed and merged about an hour ago, coincidentally. In addition to type checking the object's fields, this also revealed a couple of places where an attribute was prematurely being pulled from an object whose descendant actually had that attribute. Very cool.

TL;DR: It can be done, huge thanks to Sequoia McDowell for that article which finally set me on the right track.

like image 119
cgm123 Avatar answered Oct 03 '22 08:10

cgm123