TypeScript adds to programmer productivity and expressiveness, and is not dying

; Date: Thu Sep 21 2023

Tags: Node.JS »»»» TypeScript

Recently the teams for a couple front-end libraries abandoned using TypeScript and returned to vanilla JavaScript. That leads some to claim TypeScript is dying, and anyway that it doesn't provide useful productivity advantages to developers. While I do not have data on software development trends, I do find TypeScript adds significantly to my development work, without costing significant time.

JavaScript's nature is a permissive programming language. Anything can be anything which means we can do anything we like. While that's great for freeing up creative juices to flow and create great things, there is a lot of computer science theology behind the idea of a compiler helping the programmer to detect errors early. The bug that's cheapest to fix is the one the developer finds before committing code to the source code repository.

Type checking is able to immediately help programmers to find/fix certain classes of error. But, that comes with the "cost" of adding type declarations to code, and a compile/build step.

Supposedly that is an onerous burden making it so hard to write TypeScript code that experienced programmers shy away from using it.

In my case, in my projects, that's not the case, and I find TypeScript adds useful immediate feedback about potential errors. The type checking in TypeScript is not like the onerous burden Java imposes, for example.

I started JavaScript for real in 2010 when the team I worked in at Yahoo was reassigned to work on Yahoo's early experiment in adopting Node.js. Previously, I'd spent 10 1/2 years working in the Java SE team at Sun Microsystems, meaning I worked at the core of the Java ecosystem and personally knew the leading lights. I came to JavaScript with my mind thoroughly wedded to rigidly defined classes and data types. During my time at Yahoo, Ryan Dahl gave a critical presentation about Node.js to the engineering staff at Yahoo, who then decided to adopt it, directly causing the reassignment of my team to work on a Node.js application development platform named Manhattan. At the same time I was approached by Packt Publishing to write a book called Node.js Web Development that's now in its (www.amazon.com) fifth edition. In other words, I dove in head first into JavaScript (Node.js) and loved it.

I wrote about this journey elsewhere ( (medium.com) Why is a Java guy so excited about Node.js and JavaScript?). One of the comments to that article discussed TypeScript. Eventually I took a look, liked what I saw, and have adopted it for my own use.

TLDR; The high points

Is TypeScript dying? No. But, to be real, I work on my own and don't track any kind of statistics about TypeScript versus JavaScript.

Is TypeScript useful for programmer productivity? Yes. But, my experience using it on Node.js may not translate to developing front end code. A front end engineer may have a different opinion.

Does TypeScript cause an unreasonable coding burden? No. How long does it take to type ": number" or the like?

Does TypeScript cause an unreasonable infrastructure burden? Maybe. This is where front-end and Node.js developers may disagree. With Node.js, I keep a terminal window running tsc -w and another using nodemon to automatically restart my server. Fast, easy, no problem. The front-end engineer must rebuild their code and redeploy it to a browser.

Is TypeScript hard to learn? No. It is JavaScript with extra syntax, so you can stick with plain JavaScript if you like and adopt parts of TypeScript at a time. Type or Class declarations are easy to understand, in their simple form, if you've had experience with other languages.

Is TypeScript dangerous to software development because it comes from Microsoft? Uh... The old Microsoft followed an extend/embrace/extinguish model to attempt long-term domination of the software engineering industry. The open source model ended Microsoft's plan, and the new Microsoft seems to be an honest participant. Maybe.

For example, Microsoft seems to be honestly using TypeScript to experiment with features to propose to the ECMAScript Committee. See (techsparx.com) Microsoft proposes bringing TypeScript-like type syntax to JavaScript for a concrete example. The proposal is for JavaScript compilers to recognize, but not do anything with, type decorators like ": number".

Do you gain productivity by not compiling TS to JS and not using type checking? Not really. Using vanilla JavaScript means not having type checking. To catch the errors that type checking surfaces requires that you build more test cases. You must therefore create those test cases, and spend more time running them.

How do I use TypeScript to improve my productivity?

I do not, as said earlier, have data about trends in software development tools. I have been around long enough to have seen many "____ is D~E~A~D" claims, and to know better than to buy into that form of hype.

There is still a purpose for COBOL, Perl, PHP, Java, etc, even though many articles have been claiming their deaths.

Instead, I want to focus on why I find TypeScript to be useful, and how I use it in my workflow.

Writing good type declarations. It is useful to write, as code, the definition for objects used in code. It's a useful reference while writing other code to remember the shape of that object.

Further, the type definition syntax in TypeScript is very expressive. For example:

export type SubscriptionObjectType 
    = 'PROGRAM' | 'EVENT' 
     | 'REPORT' | 'SUBSCRIPTION'
     | 'VEN' | 'RESOURCE';

export type SubscriptionOperationType 
    = 'GET' | 'POST' 
    | 'PUT' | 'DELETE';

export type Subscription = {
    clientName?: string;
    programID?: string;
    objectOperations: Array<{
        objects?: Array<SubscriptionObjectType>;
        operations?: Array<SubscriptionOperationType>;
        callbackUrl?: string;
        bearerToken?: string;
    }>;
    targets?: Array<valuesMapItem>;
};

I find this to be a clean and succinct way of describing an object my application uses. The original definition is dozens of lines of OpenAPI schema that is nowhere near as clean.

If nothing else, writing this definition helped me to better understand the actual schema.

With this definition I can write the following:

const sub = { ... } as Subscription;

And the VS Code editing environment will immediately tell me if the object conforms.

Ignoring classes, unless necessary. A main feature of TypeScript are class declarations feeling somewhat like Java or C# classes. The "Java Guy" inside me might feel comfortable with writing class definitions. But, don't fall into the trap that everything must be modeled into a neatly defined class hierarchy.

A problem with Java development is the encouragement to overthink defining, and rigidly abiding by, class hierarchies supporting every possible nuance. TypeScript is at its heart JavaScript, and we must not lose sight of the permissiveness which enables us to freely write code expressing our intention.

A question I keep asking in my current project is whether a particular kind of object truly needs to be defined as a class. So far, I've not found a case which fits. Instead, simple type definitions have been enough.

Adding types to function interfaces and other critical items. Types are not required on every last everything. TypeScript does an excellent job of inferring the type. But, it is useful to add ": type" declarations to certain things like function signatures and the definition of certain variables.

Manually adding runtime type checking (Joi, etc). TypeScript's type checking is only active at compile time. An application receiving data may be given badly formatted data. Incoming data is not checked at compile time.

An early classic in software engineering, Elements of Programming Style by Kernighan and Plauger, includes a chapter talking about defensive programming. That's about checking function parameters before blindly using the data because of the potential of catastrophic failure.

In other words, judicious use of runtime data validation makes your application much safer. If you've incompatibly changed an object definition, data validation should quickly catch the change.

In another article, " (medium.com) Runtime Data Validation in TypeScript using Decorators and Reflection Metadata", I discuss a package (runtime-data-validation) I've developed allowing one to add runtime data validation decorators to TypeScript code. Decorators are similar to the Annotations features in Java or C#. But, the decorators implementation in TypeScript only applies to Classes, and since my current application uses only type declarations these decorators are not useful.

The Joi package has proved to be very useful and easy to use. For example, the Joi code for the above data type is:

export const joiIsSubscription = Joi.object({
    ID: Joi.forbidden(),
    id: types.joiIsObjectID.required(),
    createdDateTime: types.joiIsTimestamp.optional(),
    modificationDateTime: types.joiIsTimestamp.optional(),
    programID: types.joiIsObjectID.required(),
    clientID: Joi.forbidden(),
    clientIdentifier: Joi.forbidden(),
    clientName: Joi.string().optional(),
    resourceOperations: Joi.forbidden(),
    objectOperations: Joi.array().items(
        Joi.object({
            resources: Joi.forbidden(),
            objects: joiIsSubscriptionResource,
            operations: joiIsSubscriptionOperation,
            callbackUrl: Joi.string(),
            bearerToken: Joi.string()
        })
    ),
    targets: joiIsValueMapArray.optional(),
});

These are easy to write, and are fairly expressive. Using forbidden makes it easy to change field names and to catch code or data using the old field names.

Remember that writing a type declaration helps in understanding the object schema. Writing the data validation yourself deepens to your understanding of the schema.

Use type guard functions. A core feature of TypeScript is the type guard concept. It's simply a function with this shape:

export function isSubscription(data: any): boolean {
    // check if the object conforms with
    // the schema
    //
    if (conforms) return true;
    else return false;
}

The implementation of runtime type checking is to simply sprinkle isTypeName calls throughout your code.

In the TypeScript online documentation, read the page on Narrowing for more examples. One purpose is helping the compiler to better understand your types. But, this adds runtime validation.

Using Visual Studio Code along with the TypeScript compiler. VS Code comes with TypeScript support baked in. When it sees an available TypeScript compiler even more support is turned on.

The best part of this is, when the type of something is changed, VS Code automatically highlights all uses of that thing. Changing a function signature is then very easy, because you simply visit all the places highlighted by the editor to make the necessary changes.

Automatic rebuilds and test runs. To simplify my workflow, I keep several terminal tabs running full time. These are regular terminals rather than the VS Code terminal. The windows to use are:

  1. Running tsc -w in the server source tree to rebuild the source code.
  2. Running nodemon in the server source directory to restart the server when the built code changes.
  3. Running tsc -w in the test source tree to rebuild test source.
  4. Running nodemon in the test source to rerun the test suite on code changes.

This means an automatic rebuild faster than the time it takes to switch windows to rerun a manual ad-hoc test. And when test code changes, the test suite is automatically executed.

Where is the development overhead? It's easy to automatically rebuild the code and to rerun the test suite. IT IS EASY. Any creatively lazy programmer can replicate this workflow. Here's some hints for your package.json:

  // server source
  "scripts": {
    "build": "npx tsc",
    "watch": "npx tsc -w",
    "build-docker": "docker build . -t esx-server:latest",
    "esx": "ESXLOGDIR=`pwd`/logs CONFIG_VTN=`pwd`/vtn-config/config.yml MONGODBURL=mongodb://root:...@NNN.NNN.NNN.NNN:27017 node dist/esx-server.js",
    "devesx": "npx nodemon --watch ./dist --watch ./openadr-esx-api.yml --watch ./esx-api.yml --exec 'npm run esx'"
  },

  // test source
    "scripts": {
    "build": "npx tsc",
    "watch": "npx tsc -w",
    "test": "MONGODBURL=mongodb://root:...@NNN.NNN.NNN.NNN:27017 MONGODBNAME=ESXTEST npx mocha ./dist/config.js ./dist/programs.js ./dist/events.js ./dist/subscriptions.js ./dist/reports.js ./dist/flat-reports.js ./dist/vens.js ./dist/vcn-tree.js ./dist/targets.js",
    "monitor": "npx nodemon --watch ./dist --watch ../../server/dist --watch ./config.yml --watch config-subs.yml ... --exec 'npm run test'"
  },

Summary

Do not get caught up in any hype about software development tools.

A form of that idea is the admonition to never trust version 1.0 of anything. The early version of something might get a lot of attention, but it's rarely mature enough to be stable in production use. More importantly, as professional software engineers we must carefully consider our technology tool choices.

Rather than getting caught up in hype waves, stay true to your actual programming needs.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)