Coding Standards - Syntax-Meridian/TesseractCMS GitHub Wiki
- The codebase must remain in
strict
mode for TypeScript (tsconfig.json
) under all circumstances. - Prettier should be used to format the code prior to commit.
- Coding must comply with standard ESLint rules; any violations of ESLint must be addressed before PRs are merged.
- Additional guidelines are outlined below.
Avoid var
since let
and const
are block-scoped and more predictable.
Bad
var x = 10;
Good
let x = 10;
Prefer to specify types except where the given type is obvious from assignment.
Bad
let x: string = "Hello"; let y = getConfiguration();
Good
let x = "Hello"; let y: DatabaseConfig = getConfiguration();
Use interfaces to define shapes of objects for type annotations.
Bad
let x: { name: string; age: number; } = ...
Good
interface Person { name: string; age: number; }
let x: Person = ...
Defining functions with the function
keyword alters the meaning of this
in scope. Arrow functions maintain the parent's this
binding.
Bad
class Foo {
const bar = 10;
calculate(arr: number[]): number[] {
arr.map(function(item) { return item + this.bar; }); // this belongs to function and will return undefined
}
}
Good:
class Foo {
const bar = 10;
calculate(arr: number[]): number[] {
arr.map(item => item + this.bar); // this belongs to Foo and returns 10
}
}
Use specific types or generics. Define interfaces if necessary.
Bad
let data: any;
Good
let data: string | number;
Combine multiple types when variables can have more than one type and the type will be used in multiple locations.
Bad
function display(input: string) {}
Good
type InputType = string | number; function display(input: InputType) {}
Organize and scope your code using modules. Modules should contain classes and components that work together to deliver a specific functionality.
Example:
Given some module foo
, make folder src/foo
with contents as such:
// src/foo/FooComponent.ts
export class FooComponent {}
// src/foo/FooSound.ts
export class FooSound {}
Always specify function argument types.
Bad
function greet(person) {}
Good
function greet(person: string) {}
Prevent modification after object creation.
Bad
interface Config { option: string; }
Good
interface Config { readonly option: string; }
Makes your intent clearer and works better with optional chaining. Undefined has better defined behavior under most circumstances and will avoid footguns related to type coercing and equivalencies.
Bad
let name: string | null = null;
Good
let name?: string = undefined;
Provide a clear and defined set of related constants by using typed unions or const keyof
values. This avoids serious problems with the enum
type. There are rare occasions where enum
may be useful, but in most cases they can cause issues.
Bad
enum Color { RED = "Red", BLUE = "Blue" }
Good
Standard union type, when you don't need to iterate over the values:
type Color = "Red" | "Blue"
Const keyof
Alternative, when you need to be able to iterate over the values in the enum-like union:
const Color = {
Red: 'Red',
Green: 'Green',
Blue: 'Blue'
} as const;
type Color = typeof Color[keyof typeof Color]; // 'Red' | 'Green' | 'Blue'
for (const color of Object.values(Color)) {
// do something
}
This syntax is simply clearer.
Bad
<string>value;
Good
value as string;
Always specify return types for clarity and type safety.
Bad
function greet() { return "Hello"; }
Good
function greet(): string { return "Hello"; }
Safely access properties without explicit undefined checks.
Bad
if (obj && obj.prop) { ... }
Good
obj?.prop;
Fall back to default values for undefined
and null
via nullish coalescing. ||
incorrectly treats falsey values like 0
as null
and will erroneously assign the fallback.
Bad
const result = value || "default"; // if value = 0, result will be "default"
Good
const result = value ?? "default"; // if value = 0, result will be 0
Use named constants instead of repeated number literals.
Bad
if (status === 200) { ... }
Good
const OK_STATUS = 200; if (status === OK_STATUS) { ... }
Meaningful names improve code readability.
Bad
function fn(n: number): number {}
Good
function square(number: number): number {}
Prefix private class members with an underscore.
Bad
private name: string;
Good
private _name: string;
Functions should not modify global variables or states.
Bad
class Foo {
setName(name: string) {
globalName = name;
}
}
Good
class Foo {
private _name: string;
setName(name: string) {
this.name = name;
}
}
†There are rare cases where it is acceptable to use global state. These are generally for shared pools, singleton libraries, etc which are for a specific context (eg, lightweight lambda functions will share globals to reduce garbage collection cycles.) Almost always there is a better way via dependency injection or parameters. Talk it out with the team before you introduce globals anywhere.
Type guards let you narrow a type down from a type union or parent class to a specific union type or derived class. For classes, this can be done with typeof
. For narrowing a union of interfaces, you'll need to use type discriminators like kind
for discriminated unions.
Example:
class Foo { foo(): number }; class Bar { bar(): number }; class Baz { baz(): number }
type Input = Foo | Bar | Baz;
function process(input: Input): number {
if(typeof input === 'Foo') {
return input.foo();
} else if(typeof input === 'Bar') {
return input.bar();
} else if(typeof input === 'Baz') {
return input.baz();
}
}
Clearly indicate public class members using the public
keyword.
Bad
name: string;
Good
public name: string;
More readable when combining variables with strings.
Bad
'Hello, ' + name + '!';
Good
Hello, ${name}!
;
## **Async/Await Over Promises**
Async/await is almost always more readable than using Promise chains like `.then()`.
**Bad**
```typescript
function fetchData(): Promise<Data> {
let promise = callApi();
return promise.then(data => { return new Data(data.value + 10) });
}
Good
async function fetchData(): Promise<Data> {
const result: Data = await callApi();
return new Data(result.value + 10);
}
Avoid overloading unless necessary for public APIs. As a general rule, a function should do one thing only; if you need to accept multiple input types, use type unions.
Bad
function display(value: string); function display(value: number)
Good
function display(value: string | number)
Tuple types ensure that the semantic meaning carries along with the data being held.
Bad
let x: [string, number] = ["John", 500];
Good
type PlayerScore = [string, number]; let x: PlayerScore = ["John", 500];
Provide context and type information with JSDoc comments.
Example:
/**
* Fetches data from the API. Rate limits may vary by environment.
* @param {string} message The user's chat message
* @returns {ChatResponse}
**/
function callApi(message: string): ChatResponse {
// ...
}
Provide parameter defaults directly in the function signature rather than assigned in the function body.
Bad
function greet(message) { message = message || "Hello"; }
Good
function greet(message = "Hello") {}
Much more readable way to create a copy of an object or array. (Beware that this is a shallow copy. Use a utility library for deep copying instead.)
Bad
let clone = Object.assign({}, obj);
Good
let clone = { ...obj };
Other good references for style can be found below.