Back

Typescript, VSCode, JSDoc - An overview on managing coding style and restricting developers

Javascript and Typescript , the unneeded debate as to which is better and which should be used has been around for as long coffeescript existed and the debate shifted to Typescript once typescript started getting a lot more heated.

Anyway, none of my business.

Getting to what I use and why I use it. Oh and before we start

"JS DEVELOPERS DON'T LIKE STRICT TYPING!!!"

is an argument you can use somewhere else, I work with Go and Rust and I like strict typing so that argument doesn't work with me.

Types and Strict Typing

The problem we have with JS is that it can be variable typed and that causes issues that making debugging hard and it's not because JS allows you to do it but it's because developers need to understand what they are to write and when it's acceptable. Obviously this helps as a reviewer but then the actual developer doesn't understand the reason for the restriction

eg:

let a = 1

// in code somewhere
a = 'string value'

// in some other function
a += 2

You can see the issue here because I've segregated it to 3 specific lines where the mutation is causing the unexpected behaviour but this won't be easy to do when working with code that's larger and this is where you have 2 options.

  1. Use typescript and strictly type everything
  2. Understand the issue is with my code style and improve it.

1. Typescript

This is the easy way out and you can leave it on the TS-server to decide how it helps you with configurations and I would advise you learn TS whether or not you use it. The problem is , that without the ts-server you are basically on your own and you'll have to fallback to your own coding skills to make sure the code is safe but it's always good to have some form of completion to help you so we'll get to that as well.

2. Improving your code style

eg:

const a = 1 // cannot be changed.

a = 'string value' // will throw an error

const b = a + 2 // expected behavior b = 3

Now, the issue with this is the amount of memory you use but since most JS runtimes have a GC(Garbage Collector) that can keep the memory in control it is still worth it to know that doing this extensively on a global scope is not a good idea. Keep the memory allocations inside smaller scopes and moving to the next point, smaller broken down logic blocks.

eg:

// file: a.js
async function fetchUser() {
  const userId = 2 // would mostly come from the calling function as a param
  const resp = await serverReq(userId)
  return resp
}

// file: b.js
async function fetchUserWithImage() {
  const userId = 2 // would mostly come from the calling function as a param
  const resp = await serverReq(userId)
  const imageURL = await imageReq(resp.profile_pic_id)
  return { user: resp, imageURL }
}

Yes, yes I can import the first one and use in the second one and that's possible and a good solution in this scenario but i'm giving an example as to what I mean by code that can be easily cloned. Things that don't need much modification to reproduce similar behaviour.

What does this do? You now have 2 functions, who's memory usage is defined by themselves, resp doesn't clear up totally since the reference is passed to the above function but the internal definitions are cleared as soon as the block ends.

So, in a way a little more control over the memory usage (not as granular as something like C but it's okay for now)

const a = [1, 2, 3]
const b = a
b[0] = 1

console.log(a, b)

// next snippet
const x = {
  y: 1,
}

const z = x
z.y = 3

console.log(x, z)

People who understand the issue here already understand references, and people who think that changing b has no effect on a and changing z.y has no effect on the value of x.y , here's what's going on.

JS works with references when working with complex types, which in the generic runtime are arrays and objects, these internally return a reference point for the runtime,

eg:

const a = [1,2,3] // returns referencePoint x12132 <= some random address in the runtime memory

when you assign it to another variable, the variable takes the reference.

const b = a // `b` now points to `x12132`

And well, now any changes you make to B are made on the actual reference so unexpected results when re using the original array or original object.

How do you avoid this?

The solution is cloning and this can be a whole different post but for now, know that you create a new reference of the complex types using either additions from the newer tc39 proposals from es6 to esnext or use libraries like lodash or underscore to create clones for you.

When you understand this , you avoid most of the issues of cleaning up array and other requirements. This concept also helps with React and react's state management or Angular and Angular's ngChange directives since they both use reference comparison to see if something changed or not.

Next?

Next would be to combine these few points and see the difference, the other things that bother developers is that code style needs to be managed and consistent, for this , if anyone observed most of my projects have a formatting action that runs standard / prettier and commits back to the code in case of a PR or an edit from the github editor.

This makes sure I can make changes from anywhere and my actions would take care of handling the code style and standard is also in the commit hooks to avoid me making obvious mistakes. Do i need to make it super restrictive? Not really, the point of linters and code formatters is to show you what a simple set of rules can help you with, depending on them to handle other people's coding will just stop those developers from learning what went wrong.

A few people can learn by just assuming based on the given explanation but others need to practically see the code break to understand what they did wrong and 9/10 times they won't repeat that.

But but! I like the autocompletion from Typescript!?

Um, if you structure you app and code well enough, VSCode is smart enough to help you with auto completion and I have no issues with Typescript , i have issues with it trying to be it's own language. It went from being a strict type engine to a full fledged superset and that adds up work.

eg:

export type PartialState<
  T extends State,
  K1 extends keyof T = keyof T,
  K2 extends keyof T = K1,
  K3 extends keyof T = K2,
  K4 extends keyof T = K3,
> =
  | (Pick<T, K1> | Pick<T, K2> | Pick<T, K3> | Pick<T, K4> | T)
  | ((state: T) => Pick<T, K1> | Pick<T, K2> | Pick<T, K3> | Pick<T, K4> | T)

The above is an implementation of a PartialState type which will allow the keys to be any of the following from the State type and also extend the type T if the key is from the type T , and then I allow picking them , so your auto completion would work as expected.

The problem here? this needs to be learned and can get more and more complex as time goes, compared to simply writing the type of what the particular object / type is to represent, because this is trying to be more than a type, this is trying to be the entire logic of how the code can be accessed, and is specifically written to help people write function parameters the way I want to restrict it too but then , is this readable over time? If I come back to this 2 years later will I understand what I was thinking? Probably not.

But yes, I understand that intellisense is a huge part of developers life today and so I use an alternative, I don't write types this complex. I write smaller types that are basically easy to read and use and use them in my .js files.

Wait, WHAT?

You read it right, I use the types in my .js files. The ts-server is a very powerful tool and even more powerful tool today is the code editor VSCode and it handles typescript natively since it's built on the same tech.

But , it supports the general JS ecosystem to it supports JSDoc and typescript itself supports JSDoc since that was the de facto way of writing documentation for JS before all of this came up.

Get to the goddamn example already!

Cool, so since ts-server can handle both, I can write types in TS and keep using JS for my logic block without ever having to setup typescript in the project, I don't need tsconfig, i don't need to compile my code, or sit and solve type issues that aren't supposed to be blocking when it's Friday and you have to deploy the project in an hour.

so , this is how I have it setup as a small example

// app.js

/** @returns {import("./types").SomeTypeDef} */
function printSomeTypeDef() {
  return {
    name: 'hello', // will show the autocomplete if I type `n` as the return type is defined already
  }
}

// types.d.ts
declare interface SomeTypeDef {
  name: string
  age: number
}

Nah, don't get scared, the import statement will be autofilled by vscode so it's okay. Also, minor detail, you can directly write {SomeTypeDef} without importing in cases where you have a single ambient type declaration file, if you have any other type declaration file that does exports instead of ambient declaration, you'll have to use the import syntax.

That's a lot of typing.

Not really, it's the same amount of typing you'd do to import a type in TS and then assign it to a function, also most of JSDoc syntax is also autocompleted by VSCode without the need for a ts-server so that's that. The issue with this is the need for VSCode, for someone like me who works with Vim, Sublime as and when I feel like it, the autocompletion breaks and that's fine because I'd prefer referring and falling back on my own coding skills than totally depending on typescript to decide what I can do in my code.

Finally,

Probably typed a lot of things that will offend people but these are based on my experiences and based on things I've screwed up while learning to code, might differ in your case and you may have valid counter points to everything I've written and I'd honestly like to hear them. Till next time...