How to write types for better usability
Tags: webdev, javascript, typescript, beginners
Published: 2023-10-15 14:09:44

TypeScript is a very popular JavaScript tool with over 94.7k GitHub stars as of the time of writing.
Of course, there are also 5,676 open issues, of which only 1,693 are labeled as bugs.
I will briefly mention the problems with this, but it is still a very useful tool.
The important fact is that the usability of TypeScript is greatly affected by how well the types are written.
In this article, I will introduce some tips for TypeScript and JSDoc that I have used while actually writing simple libraries.

## More intelligent union type
Let's consider the case of defining Bird and Fish types simply.
The `Animal` type has a function called `eat`, and `Bird` and `Fish` have functions called `fly` and `swim`.
```ts
type Animal = {
  eat(food: any): void
}
type Bird = {
  type: "Bird"
  fly(): void
} & Animal
type Fish = {
  type: "Fish"
  swim(): void
} & Animal
```
The above types seem appropriate, but they are not very convenient.
Let's write a function called `move`, for example.
```ts
function move(animal: Animal) {
  if (animal.type == "Bird") {
    (animal as Bird).fly()
  } else if (animal.type == "Fish") {
    (animal as Fish).swim()
  }
}
```
Logically, it seems possible to use `type` values to use type guards, but it is not actually the case.
The following is how to fix the above types to work logically.
```ts
type BaseAnimal = {
  eat(food: any): void
}
type Bird = {
  type: "Bird"
  fly(): void
} & BaseAnimal
type Fish = {
  type: "Fish"
  swim(): void
} & BaseAnimal
type Animal = Bird | Fish
```
To use type guards, Animal must be made a union type.
[Real example: dom-eater](https://github.com/Artxe2/lube-series/blob/master/packages/dom-eater/public.d.ts)

## Support for stronger type checking using `infer`
`infer` keyword is a function that can be used to infer types in code using the extends keyword.
By using infer to implement types such as Parameters and ReturnType, or by directly using infer, you can provide even more sophisticated and powerful type support.
For example, to strengthen the type check of `setTimeout` using `Parameters`, you can write the code as follows.
```ts
declare function setTimeout<T extends (...args: any[]) => any>(handler: T, timeout?: number, ...arguments: Parameters<T>): number

// Argument of type 'number' is not assignable to parameter of type 'string'.ts(2345)
setTimeout((a: string, b: number) => a + b, void 0, 123, 123)

// is ok
setTimeout((a: string, b: number) => a + b, void 0, "abc", 123)
```
In fact, `setTimeout` can execute `eval` by passing a string as a callback, but we will not consider this part.
[Real example: async-lube](https://github.com/Artxe2/lube-series/blob/master/packages/async-lube/private.d.ts)

## Avoid `d.ts` and optimize the `ts.map` by maximizing type inference.
Defining `d.ts` files for complex type types is not only a very tedious task, but it also makes IDEs move to `d.ts` files instead of actual sources when using the source tracking function using `ts.map` files.
If you use the `infer` keyword and `typeof` keyword introduced above to make the most of the type inference of actual code, you can define types more simply, support strong type checking, and also enjoy the benefits of `ts.map` files.
I will only attach actual usage cases on this topic and move on.
[Real example: async-lube](https://github.com/Artxe2/lube-series/blob/master/packages/async-lube/src/src/decorator.js)

## Do not be surprised at the bugs in TypeScript
As mentioned earlier, TypeScript has a lot of bugs.
One of the critical bugs I encountered is that when using the typeof keyword in a generic function when generating `d.ts` files using `tsc` and JSDoc, the compiler does not reflect it properly and converts it to any type.
However, there are many bugs in TypeScript, and [contributing to the project is quite cumbersome](https://github.com/microsoft/TypeScript/blob/main/CONTRIBUTING.md).
Even to report a bug, you need to search the issue list of thousands to see if it is already a known bug.
If you encounter a bug, the best way is to create a PR to fix it, but also consider clunky solutions.
I solved the above bug by [writing a script to convert the returns type of any type JSDoc.](https://github.com/Artxe2/lube-series/blob/work/scripts/async-lube_fix_types.js)
[autogenerated d.ts](https://github.com/Artxe2/lube-series/blob/work/packages/async-lube/types/src/decorator.d.ts)
I will attach a playground for some of TypeScript's bugs.
[TypeScript Bugs](https://www.typescriptlang.org/play?#code/PTAEEsBcHIGdQMYHsBOKCmDIChkDtZJQNYkAbAN3QC5QAFFJAW3FnQB48BXJgI3RQA+UAF56jFmwB0JclQAUDZq3Qz0pSukUSVajQoAMAShPYzAQ1gBPPAlAAzLrcjgkeUAAcU7ACqD5FLQ+RqAA3tigxOiQXCjuFNgAvtggEDDwsAAWSFxkACag-OLKbJw8-EK4boSeOmy0SpIcjSrsLaXcfAKCPaK18l4DKPLGpmb4Nea05nhWfQbYANzy5qCWxU1tdc3bZV1CPUZSkJnoePIRoKkU5mRc6BAZ2bkFRZ0VlwG39w277RzvbqCEIiYShZJGMypcB4PLoAAej1ASA8Ljct2wACJLHlMVIEJlzCgAIKQeRGIA)
[JSDoc d.ts Bugs](https://www.typescriptlang.org/play?filetype=js#code/PQKhCgAIUgXALAlgZ0gYwPYFssFMB2skKkAJigIYAOVuFATsgHRQwACsuWVANhZ5ADeACiZiGAc2QAuaAG0AugEpIAXgB80AL6QAKq0hsqDCliG6daCjx4AjCmgDWrYOFwAPKhnpFSuAGYUAK48RMJWNvZOKhpCUJCgEJDJMAgkmDgERCTkyNS0DKiwGBgsydCGxvSmQgAKJnicjAA8uuo6FPHQrsmY+MhEQbCIPGqQwhQxmhF2Do4TSuDxiV0wJBjO5SA9kEMjTP74Y8JTQlpLyfS4sEH0R3s84OfgwMDEqBtunt5EfQPoiHoaBC-EQGCOqnGp0E8T+g2Go0hgnOyRWW0MVxud1QggAStdbvhdABPWjNWCk3AYfwAoEg4bg9qrHYPA4QqFqTQPeKYwm7BFPJZo9icbh8ASCAb0RD4CSQAA+kHwQSwtlw9B0+gqRga5h0VBcXy8PnQ4P+aEBwJ4AH0JAR1Yg0McqNDYWb4SMxsjlmBVu8lRgiBR8MS-WxedihPisUTKeTKdTaVbbfbpWhWuomVsWQi2cdTtyfUkUv6NgB+ANiMol8MEyN4uuxskU2iJi10m12-AOtBZlI5-b+IL4J2Qk6c-kjHmNyePLRAA)

## Simple tips for using type conversions in JSDoc
The syntax of JSDoc is definitely not as good looking as TypeScript, but the most unique one is the type casting syntax.
[TypeScript adopted this syntax from Google Closure](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#casts), but I think it would have been better to create a syntax like `@cast`.
The simple solution I came up with for this problem is to add additional comments after the type casting syntax.
```ts
// ts
const value = func(something as SomeType)

// JSDoc
const value = func(/** @type{SomeType} */(something)) 

// JSDoc with extra comment
const value = func(/** @type{SomeType} */(something)/**/)
```
It may seem even more cumbersome, but I personally recommend this method because the readability of the code was greatly improved when I used it.

## Conclusion
TypeScript has become a very convenient tool over the past 10 years.
If you handle it in the right way, as introduced in this article, TypeScript will give you great satisfaction when writing code.
However, the many issues that exist in the TypeScript project have dampened my willingness to contribute and made it difficult to actually contribute to the project.
I commented on the issue, but since the existing issue was reported several years ago, I don't expect it to be resolved soon.
Nevertheless, TypeScript is already a very attractive tool, so I hope you will enjoy the convenience that this tool provides.
Thank you.