TypeScript Style Guide
Reference: “https://google.github.io/styleguide/tsguide.html”
Section titled “Reference: “https://google.github.io/styleguide/tsguide.html””1. Project Structure
Section titled “1. Project Structure”- Folder and File Names: Use
kebab-casefor file and folder names (example:my-module.ts). - Modules: Organize code into coherent modules and group related files.
2. Naming Conventions
Section titled “2. Naming Conventions”- Classes and Interfaces: Use
PascalCasefor classes and interfaces (MyClass,MyInterface). - Variables and Functions: Use
camelCasefor variables and functions (myVariable,myFunction). - Constants: Use
UPPER_CASEfor constants (MY_CONSTANT).
3. Declarations and Types
Section titled “3. Declarations and Types”-
Types: Define types explicitly.
let name: stringlet age: numberlet isActive: boolean -
Interface vs Type: Use interface for objects, classes for complex types, and types for less complex objects.
interface Person {name: stringage: number}class Person {name: stringage: numbersetAge(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}4. Code Formatting and Style
Section titled “4. Code Formatting and Style”- 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}5. Development Practices
Section titled “5. Development Practices”-
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 handlingconsole.error(error)} -
Unit Testing: Write unit tests for your functions and classes.
6. ## Importing and Exporting Types
Section titled “6. ## Importing and Exporting Types”-
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.
-
Exporting Types
Section titled “Exporting Types”Use
export typewhen 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.
7. Class Members
Section titled “7. Class Members”-
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).
8. Use export type to re-export types
Section titled “8. Use export type to re-export types”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.
export interface AnInterface { name: string age: number}// bar.tsexport 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.
export interface Foo { property: string}
// bar.tsexport interface Bar { property: string}
// index.tsexport * 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 recommendedexport default function foo() {}
// Recommendedexport 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}13. Avoid using any
Section titled “13. Avoid using any”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 recommendedlet variable: any
// Recommendedlet variable: string | number14. Use enum with caution
Section titled “14. Use enum with caution”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.
// Recommendedconst Directions = { North: 'NORTH', South: 'SOUTH', East: 'EAST', West: 'WEST'} as const
type Direction = (typeof Directions)[keyof typeof Directions]
// NOT recommendedenum DirectionEnum { North, South, East, West}15. Use composition over inheritance
Section titled “15. Use composition over inheritance”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 recommendedclass Animal { move() { console.log('The animal moves') }}
class Bird extends Animal { fly() { console.log('The bird flies') }}
// Composition recommendedclass Animal { move() { console.log('The animal moves') }}
class Bird { private animal = new Animal()
fly() { console.log('The bird flies') }
move() { this.animal.move() }}16. Use unknown instead of any
Section titled “16. Use unknown instead of any”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 recommendedlet value: anyvalue = 'string'value = 42
// Recommendedlet value: unknownvalue = '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 recommendedconst DIRECTIONS = { North: 'NORTH', South: 'SOUTH', East: 'EAST', West: 'WEST'}
// Recommendedconst 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 recommendedarray.forEach((item) => { if (item === 3) return // Does not work as expected console.log(item)})
// Recommendedfor (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 recommendedfetch('https://api.example.com/data') .then((response) => response.json()) .then((data) => console.log(data)) .catch((error) => console.error(error))
// Recommendedasync 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) }}20. Avoid global variable declarations
Section titled “20. Avoid global variable declarations”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 recommendedvar globalVariable = 'value'
// Recommendedfunction myFunction() { const localVariable = 'value' console.log(localVariable)}