Skip to content

TypeScript Style Guide

  • Folder and File Names: Use kebab-case for file and folder names (example: my-module.ts).
  • Modules: Organize code into coherent modules and group related files.
  • Classes and Interfaces: Use PascalCase for classes and interfaces (MyClass, MyInterface).
  • Variables and Functions: Use camelCase for variables and functions (myVariable, myFunction).
  • Constants: Use UPPER_CASE for constants (MY_CONSTANT).
  • Types: Define types explicitly.

    let name: string
    let age: number
    let isActive: boolean
  • Interface vs Type: Use interface for objects, classes for complex types, and types for less complex objects.

    interface Person {
    name: string
    age: number
    }
    class Person {
    name: string
    age: number
    setAge(age: number) {
    this.age = age
    }
    }
    type ID = string | number
  • Extending interfaces using types: To improve the readability of complex or very extensive types, it is recommended to extend these types and implement them using interfaces.

type PersonDetails = {
name: string
age: number
}
interface IPerson extends PersonDetails {
setAge(age: number): void
}
  • Indentation: Use 2 spaces for indentation.
  • Line Length: Limit lines to 80 characters.
  • Use of Semicolons: Always use semicolons at the end of statements.
  • Comments: Use JSDoc comments to document the code.
/**
* Adds two numbers.
* @param a - The first number.
* @param b - The second number.
* @returns The sum of a and b.
*/
function add(a: number, b: number): number {
return a + b
}
  • Avoid Duplicate Code and Code Smells: Refactor code to eliminate duplications.

  • Error Handling: Use try…catch to handle errors.

    try {
    // Code that might throw errors
    } catch (error) {
    // Error handling
    console.error(error)
    }
  • Unit Testing: Write unit tests for your functions and classes.

  • Importing Types: You can use import type {...} when the imported symbol is used only as a type. Use regular imports for values:

    import type { Foo } from './foo'
    import { Bar } from './foo' // You can also combine like this: import { type Foo, Bar } from './foo';
  • Why?

    The TypeScript compiler automatically handles the distinction and does not insert runtime loads for type references. So, why annotate type imports?

    The TypeScript compiler can run in two modes:

    Development mode: Typically, we want fast iteration loops. The compiler transpiles to JavaScript without full type information. This is much faster but requires import type in certain cases.

    Production mode: We want correctness. The compiler checks all types and ensures import type is used correctly.

  • Use export type when re-exporting a type, for example:

    export type { AnInterface } from './foo'

    Why?

    export type is useful for allowing type re-exports in file-by-file transpilation. Refer to the isolatedModules documentation.

    export type might also seem useful to avoid exporting a value symbol in an API. However, it does not offer guarantees: downstream code could still import an API through a different path. A better way to divide and ensure the use of type vs value in an API is to actually separate the symbols into, for example, UserService and AjaxUserService. This is less error-prone and also better communicates intent.

  • Do not use #private fields: Do not use private fields (also known as private identifiers):

    class Clazz {
    #ident = 1
    }

Instead, use TypeScript’s visibility annotations:

class Clazz {
private ident = 1
}

Why?

Private identifiers cause a substantial increase in code size and performance regressions when TypeScript transpiles them to a lower level. Additionally, they are not supported before ES2015 and can only be transpiled to ES2015, not to lower levels. At the same time, they do not offer substantial benefits when using static type checking to enforce visibility.

  • Use readonly

Mark properties that are never reassigned outside of the constructor with the readonly modifier (these do not need to be deeply immutable).

Use export type when you need to re-export a type from another module. This is useful for maintaining consistency and clarity in type imports.

foo.ts
export interface AnInterface {
name: string
age: number
}
// bar.ts
export type { AnInterface } from './foo'

9. Group imports into a single block and organize them alphabetically

Section titled “9. Group imports into a single block and organize them alphabetically”

Grouping imports into a single block and organizing them alphabetically improves code readability and makes it easier to locate imports.

import { Bar } from './bar'
import { Baz } from './baz'
import { Foo } from './foo'
export interface Example {
field: string
}

10. Use export * to export multiple values from a module

Section titled “10. Use export * to export multiple values from a module”

Use export * to export all values from a module. This is useful when you want to re-export the entire contents of a module without listing each item individually.

foo.ts
export interface Foo {
property: string
}
// bar.ts
export interface Bar {
property: string
}
// index.ts
export * from './foo'
export * from './bar'

11. Prefer named exports over index exports

Section titled “11. Prefer named exports over index exports”

Named exports are clearer and easier to debug than index exports. Using named exports makes it easier to know what is being imported.

// NOT recommended
export default function foo() {}
// Recommended
export interface Data {
field: string
}
export function fetchData() {}

12. Use export default for a single export per module

Section titled “12. Use export default for a single export per module”

Use export default when there is only one main item that the module should export. This is useful for main classes or functions.

export default class MyClass {
private value: string
constructor(value: string) {
this.value = value
}
}
export interface MyClassInterface {
value: string
}

Avoid using the any type as much as possible, as it disables type checking and can introduce hard-to-detect errors. Instead, use more specific types.

// NOT recommended
let variable: any
// Recommended
let variable: string | number

Use enum only when it is truly necessary. In many cases, an object with read-only properties (readonly) can be a safer and more flexible alternative.

// Recommended
const Directions = {
North: 'NORTH',
South: 'SOUTH',
East: 'EAST',
West: 'WEST'
} as const
type Direction = (typeof Directions)[keyof typeof Directions]
// NOT recommended
enum DirectionEnum {
North,
South,
East,
West
}

Use composition instead of inheritance to reuse code and create more flexible and maintainable structures. Composition allows combining functionalities in a modular way without the restrictions of inheritance.

// Inheritance NOT recommended
class Animal {
move() {
console.log('The animal moves')
}
}
class Bird extends Animal {
fly() {
console.log('The bird flies')
}
}
// Composition recommended
class Animal {
move() {
console.log('The animal moves')
}
}
class Bird {
private animal = new Animal()
fly() {
console.log('The bird flies')
}
move() {
this.animal.move()
}
}

Use unknown over any when you do not know the type of a variable. unknown is safer than any because it forces type checks before use.

// NOT recommended
let value: any
value = 'string'
value = 42
// Recommended
let value: unknown
value = 'string'
value = 42
if (typeof value === 'string') {
console.log(value.toUpperCase()) // Safe
}

17. Use as const to create constant literals

Section titled “17. Use as const to create constant literals”

Use as const to create constant literals. This ensures that the values cannot be changed and that immutability is maintained.

// NOT recommended
const DIRECTIONS = {
North: 'NORTH',
South: 'SOUTH',
East: 'EAST',
West: 'WEST'
}
// Recommended
const DIRECTIONS = {
North: 'NORTH',
South: 'SOUTH',
East: 'EAST',
West: 'WEST'
} as const
type Direction = (typeof DIRECTIONS)[keyof typeof DIRECTIONS]

18. Prefer for...of over forEach for array iterations

Section titled “18. Prefer for...of over forEach for array iterations”

Use for...of instead of forEach when you need to iterate over arrays. for...of is more flexible and allows using break and continue, which is not possible with forEach.

const array = [1, 2, 3, 4, 5]
// NOT recommended
array.forEach((item) => {
if (item === 3) return // Does not work as expected
console.log(item)
})
// Recommended
for (const item of array) {
if (item === 3) continue // Works correctly
console.log(item)
}

19. Use await instead of then for promises

Section titled “19. Use await instead of then for promises”

Prefer using await to handle promises instead of chaining then. await makes asynchronous code clearer and easier to read.

// NOT recommended
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error))
// Recommended
async function getData() {
try {
const response = await fetch('https://api.example.com/data')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}

Avoid declaring variables in the global scope to reduce the risk of name conflicts and improve code modularity. Keep variables within their necessary contexts.

// NOT recommended
var globalVariable = 'value'
// Recommended
function myFunction() {
const localVariable = 'value'
console.log(localVariable)
}