How to Keep your TypeScript Code Clean
TypeScript brought me a lot of useful features like the ability to rename variables by reference in multiple places, spot errors in the code before even running the app, and a lot more.
Here are a few small things I do that make a big difference in code maintainability and readability.
Use primitive types
Use string, number, boolean on your variable types. Don’t use non-primitive types like String or Number. Think about this: would you assign a number max = new Number(10) or max = 10?
❌ Wrong
let max: Number
✔️ Right
let max: number
Use the type void
When your function doesn’t return any value, you can use the type void.
Not returning a value can be bad if you want to have pure functions and avoid side-effects. In that case you should avoid void.
But, in some cases, like callbacks, you can use this to prevent wrong usage of a function.
❌ Wrong
let log: (message: string) => any
log = (message) => {}
const result = log('') // allowed
✔️ Right
let log: (message: string) => void
log = (message) => {}
const result = log('') // syntax error
Place types where they belong
Place types close to where they are being used. That’s probably the right home for them.
❌ Wrong
Create a types.ts file for your entire app. A file like this is going to get big and hard to maintain.
✔️ Right
Place types in their proper file and context. For example, if you have a file that fetches data from an API and returns an object, the type definition for that data should be in the same file.
Prefer implicit types over explicit types
According to Clean Code, comments that simply repeat what the variable is saying are just noise. The same goes for types: there’s no point in duplicating the information.
❌ Wrong
const message: string = 'hello world'
✔️ Right
const message = 'hello world'
Don’t overuse the type any
The type any can be really useful or easier in many scenarios, but if you keep overusing it you’re giving up all of TypeScript’s advantages.
Once you use an any, the any-ness can leak into other parts of your code and really wreck type-safety.
❌ Wrong
Use any everywhere.
✔️ Right
Use any only when really needed. One example where it could be ok is in a third party API that might change or might return way more information than you need.
Prefer async/await over then
When using promises you can make your code linear with the async/await feature, this makes the code a lot easier to read.
❌ Wrong
foo().then(bar).then(baz)
✔️ Right
await foo()
await bar()
await baz()
Avoid export default
You can name a default export anything. This is sometimes really bad for me because it’s hard to remember what I named the same import in another place— I have to go back and check it if I want to keep consistent. And, as if this weren’t hard enough, if you refactor, it will not rename the imported names.
❌ Wrong
export default Foo
import FooComponent from './foo'
✔️ Right
export class Foo {}
import { Foo } from './foo'
Clean code is all about keeping the code simple, easy to read, easy to change.
Disclaimer: I’m using ❌ Wrong and ✔️ Right just to make it clearer what is the preferred approach, it’s not a rule that says that the approach is absolutely right or wrong.
Other Resources
- Brief summary of clean code
- Default is bad
- Example of type location in tslint Github
- Visual Studio Code has a built-in feature to help with the conversion from then to async/await