DEV Community

Cover image for How to write types for better usability
Yeom suyun
Yeom suyun

Posted on • Updated on

How to write types for better usability

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.

type Animal = {
  eat(food: any): void
}
type Bird = {
  type: "Bird"
  fly(): void
} & Animal
type Fish = {
  type: "Fish"
  swim(): void
} & Animal
Enter fullscreen mode Exit fullscreen mode

The above types seem appropriate, but they are not very convenient.
Let's write a function called move, for example.

function move(animal: Animal) {
  if (animal.type == "Bird") {
    (animal as Bird).fly()
  } else if (animal.type == "Fish") {
    (animal as Fish).swim()
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

type BaseAnimal = {
  eat(food: any): void
}
type Bird = {
  type: "Bird"
  fly(): void
} & BaseAnimal
type Fish = {
  type: "Fish"
  swim(): void
} & BaseAnimal
type Animal = Bird | Fish
Enter fullscreen mode Exit fullscreen mode

To use type guards, Animal must be made a union type.
Real example: dom-eater

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.

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)
Enter fullscreen mode Exit fullscreen mode

In fact, setTimeout can execute eval by passing a string as a callback, but we will not consider this part.
Real example: async-lube

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

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.
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.
autogenerated d.ts
I will attach a playground for some of TypeScript's bugs.
TypeScript Bugs
JSDoc d.ts Bugs

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, 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
const value = func(something as SomeType)

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

// JSDoc with extra comment
const value = func(/** @type{SomeType} */(something)/**/)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
ben profile image
Ben Halpern

Nice post