Skip to content

Latest commit

 

History

History
1359 lines (1020 loc) · 47.5 KB

style.md

File metadata and controls

1359 lines (1020 loc) · 47.5 KB

Jsdoom Style Guide

The purpose of this style guide is to keep jsdoom's source code readable, approachable, consistent, and maintainable. When contributing to jsdoom, please follow these guidelines. Pull requests containing code that does not follow the guidelines may be rejected until the code has been made conformant.

These are not strict rules! Exceptions are allowed where it improves readability. However, exceptions should be just that: Exceptional. Exceptions should be made sparingly, if they are made at all.

Please create a GitHub issue if you would like to request a change or an addition to this style guide.

You can link to a particular guideline in this style guide by using a url such as the below example, where the numbers at the end are the same as those at the beginning of the header where you wish to link. You can also copy the target URL of items in the table of contents.

https://github.com/pineapplemachine/jsdoom/blob/master/style.md#user-content-1.4.1.

Throughout this guide, specific examples of conforming code will be provided like so:

// An example of conforming code
console.log("jsdoom is pretty neat");

Table of Contents

1. Whitespace

The following guidelines mainly pertain to the use of whitespace in code.

1.1. Indent using four spaces

Code is indented using four space characters. Do not indent with tab characters.

// [Comment explaining the purpose of my function]
function myFunction(): void {
    doStuff();
    if(condition){
        doOtherStuff();
    }
}

1.2. Do not use double indents

Indentation must be in such a way that each line is indented at either the same level as the last line, one more level, or one less. There should never be a jump of two or more indentation levels between lines.

Longer expressions made up of chained function calls should be broken across multiple lines by using several shorter statements with intermediate assignments, or by putting the code inside a pair of paretheses () etc. on a new, idented line. They should not be broken across lines by starting each line with the right-hand part of a member access, e.g. having a line begin with .map(...).

myArray.map((value) => {
    return transformMyValue(value);
}).filter((value) => {
    return filterMyValue(value);
});

1.3. Do not use partial indents

There should never be a partial change in indentation. All changes in indentation from one line to the next should always be in multiples of four space characters.

1.4. Whitespace as it pertains to braces, brackets, and paretheses

The following guidelines mainly pertain to how whitespace should be used around braces {}, square brackets [], angle brackets <>, and parentheses ().

1.4.1. No characters inside empty literals, blocks, or expressions

Where an open brace {, parenthese (, etc. is followed by a corresponding closing brace }, parenthese ), etc. with no code in between, there should not be any whitespace, comments, or other characters in between those open and closing characters.

const myEmptyObject: Object = {};
const myEmptyArray: number[] = [];
myFunctionInvokedWithNoArguments();
// [Comment explaining the purpose of my unconventional loop with no body]
while(myIndex++ < myMaximumIndex){}

1.4.2. Nested code should be indented at one additional level

Where a statement is spread across multiple lines, the lines between a corresponding pair of braces {}, paretheses (), square brackets [], or angle brackets <> should be indented at one additional level.

const myMultiLineObjectLiteral: Object = {
    myFirstAttribute: 1,
    mySecondAttribute: 2,
};
const myMultiLineExpression: boolean = (
    myFirstCondition &&
    mySecondCondition
);
const myMultiLineArrayLiteral: string[] = [
    "Hello world",
    "How are you?",
];

1.4.3. Code within braces should not be in-line

Open braces { should always be followed by a newline and close braces } should always be preceded by a newline. The code in between corresponding braces should be indented at one level further than the lines containing those braces. Normally, an open brace { should not be immediately followed by any character other than a corresponding closing brace } or a newline \n.

const myObject: Object = {
    myAttribute: doStuff({
        myOption: 1,
    });
};

1.4.4. Do not put open braces on their own line

Open braces { should not be preceded by a newline. Open braces { should be on the same line as the statement or expression that they are a part of.

1.4.5. Do not pad code between paretheses or brackets

Except for where an expression is distributed across several lines due to its length, expressions or blocks inside paretheses (), square brackets [], or angle brackets <> should not have spaces in between the opening character and the first character of the enclosed expression, nor in between the closing character and the final character of the enclosed expression.

// No space in between `[` and `1`.
// No space in between `5` and `]`.
const myArray: number[] = [1, 2, 3, 4, 5];
// Newlines are okay for spreading a long statement across multiple lines.
const myArrayTooLongToFitOnOneLine = [
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];

1.5. Use a single space after colons but no whitespace before

In objects and in typed declarations, there should be a single space after each colon : and no space before it.

const myObject: Object {
    myAttribute: "hello world",
};

1.5. Use whitespace after commas but not before

In comma-separated lists, there should be whitespace after each comma , and no space before it.

If the next item in a list after a given comma appears on the same line, then there should be a single space in beween the comma and the following non-whitespace character. If the next item in the list appears on the following line, then there should be a newline immediately after the comma and the next list item should be indented at the appropriate level.

const myArray: number[] = [1, 2, 3, 4];
const myMultiLineArray: string[] = [
    "Hello",
    "World",
];

1.6. Infix operators

The following guidelines mainly pertain to the use of whitespace around infix operators, e.g. + or *.

1.6.1. Put a single space between an infix operator and its left operand

Infix operators should appear on the same line as the last character of their left operand. They should be separated from the last non-whitespace character of that left operand by a single space character.

1.6.2. Put whitespace between an infix operator and its right operand

An infix operator should have either a single space separating it from the first non-whitespace character of its right operand, or it should be immediately followed by a newline with the right operand placed at the correct indentation level on the next line.

const mySum: number = 100 + 200;
const myMultiLineExpression: boolean = (
    myFirstCondition ||
    mySecondCondition
);

2. Newlines

The following guidelines mainly pertain to the placement of new lines and blank lines in code.

2.1. Lines should not exceed eighty characters

In general, no one line of code should exceed 80 characters. Please consider eighty characters to be a soft limit and one hundred characters to be a hard length limit.

2.4. Line breaks

The following guidelines mainly pertain to where line breaks should appear in code.

2.4.6. Line breaks should not occur anywhere not mentioned in 2.4.

Line breaks on lines containing code should not occur under circumstances other than the ones explicitly mentioned below.

Comments may exceptionally appear in between a character that would normally precede a line break and the terminating newline. However, this style of comments is not encouraged. In general, comments should appear on their own lines and not at the end of a line of code. (See 4.3..)

2.4.2. Line breaks should always occur after a semicolon

Semicolons terminating a statement should be immediately followed by a newline. Two statements should not appear on the same line.

const myFirstDeclaration: number = 1;
const mySecondDeclaration: number = 2;

2.4.3. Line breaks may occur after a comma

Long lists of arguments, parameters, attributes, or other comma-separated lists may be broken up by placing newlines after the commas.

const myMultiLineArray: number[] = [
    0x0100,
    0x0200,
    0x0400,
];

2.4.4. Line breaks may occur after an open brace, bracket, or parethese

The code in between braces, brackets, etc. may appear on separate lines and be indented at one additional level.

2.4.5. Line breaks may occur after an infix operator

It is appropriate for a line break to appear after an infix operator and before its right operand.

const myMultiLineExpression: boolean = (
    myFirstCondition ||
    mySecondCondition
);

2.4.6. Line breaks may occur after the last expression in a list

The last character of a multi-line list should normally be a trailing comma. However, in the case that the list appears on one line yet not on the same line as the closing bracket ] or other character, it is appropriate for a line break to appear after the last item in the list.

const myValue: number = myMultiLineFunctionInvocation(
    100, 200, 300, 400, 500, 600
);

2.4.7. Line breaks may occur after either symbol of a ternary expression

A line break may immediately follow the first ? or second : symbol of a ternary expression, or both, when breaking the expression across several lines will improve readability.

const myTernaryExpressionResult: string = (myCondition ?
    "My first string literal" :
    "My second string literal"
);

2.2. Blank lines

The following guidelines mainly specifically to the placement of blank lines. A blank line is a line which contains no characters or which contains only whitespace characters.

2.2.1. Source files should not begin with a blank line

The first line in a source file should not be a blank line.

2.2.2. Functions and methods should be padded by blank lines

Function or method declarations should be padded on each side by a single blank line, except for where the immediately previous or following line is the first line of the class declaration containing a method, or the line containing a class declaration's closing brace }.

This does not apply to helper functions that are declared inside another function. In general, function blocks should not contain blank lines, not even to pad helper function declarations. (See 2.2.3..)

// [Comment explaining the purpose of my function]
function myFirstFunction(): void {
    doStuff();
}

// [Comment explaining the purpose of my function]
function mySecondFunction(): void {
    doStuff();
}

// [Comment explaining the purpose of my function]
function myThirdFunction(): void {
    doStuff();
}
class MyClass {
    // [Comment explaining the purpose of my method]
    myFirstMethod(): void {
        doStuff();
    }

    // [Comment explaining the purpose of my method]
    mySecondMethod(): void {
        doStuff();
    }
    
    // [Comment explaining the purpose of my method]
    myThirdMethod(): void {
        doStuff();
    }
}

2.2.3. Function blocks should not contain blank lines

The implementation of a function or method should not contain blank lines. If a function is made up of several different conceptual units, then they should be separated by explanatory comments instead of separated by blank lines. If a function is too long or complex to be readable without those empty lines, then parts of the implementation should be moved into other helper functions.

2.2.4. Logical groups of imports should be padded by blank lines

Logical groups of imports should be separated from each other by a single blank line. See 6.15. for more information about how imports should be grouped.

2.2.5. Logical groups of class members should be padded by blank lines

Logical groups of class members should be separated from each other by a single blank line. See 6.16. for more information about how class members should be grouped.

2.5. There should be a newline at the end of each source file

TypeScript source files should terminate with a newline character \n.

3. Punctuation

The following guidelines mainly pertain to the use of punctuation characters in code.

3.1. Always use semicolons at the end of statements

Statements should always be terminated by a semicolon ;. Do not trust JavaScript's automatic semicolon insertion to get it right.

3.2. Use trailing commas in multi-line lists

Where it is syntactically valid, the last item in a comma-separated list that spans more than one line should be followed by a comma ,.

const myMultiLineArray: number[] = [
    0x0100,
    0x0200,
    0x0400,
];

3.3. Always enclose block statements within braces

Block statements such as the body of a loop or an if statement should always be enclosed within braces, even if it is syntactically valid to omit those braces. Single-line conditionals or loops without braces around their bodies should not be used.

if(myExitCondition){
    return;
}
while(myLoopCondition){
    counter++;
}

3.4. Use parentheses generously when mixing infix operators

When a single expression contains several different infix operators, each group of identical operators should generally be enclosed within parentheses (). This helps to avoid any confusion or ambiguity regarding the intended order of operations.

const myValue: number = 10 * (20 + myOtherValue);
const myBoolean: boolean = (
    (firstCondition || secondCondition) &&
    (thirdCondition || fourthCondition)
);

3.5. Arrow functions

The following guidelines mainly pertain to the use of punctuation characters as they apply to arrow functions.

3.5.1. Prefer parentheses around arrow function parameters

The parameter list of an arrow function should always be enclosed within paretheses (), even when there is only a single parameter.

myArray.map((value) => {
    return value + value;
});

3.5.2. Prefer braces around arrow function bodies

The bodies of arrow functions should always be enclosed within braces {}, even when the function body contains only a return statement.

myArray.filter((value) => {
    return value > 0;
});

3.6. Strings

The following guidelines mainly pertain to the use of punctuation characters as they apply to string literals.

3.6.1. Prefer double-quoted strings

String literals should be double-quoted "". String literals should not ever be single-quoted '', even when the literal itself contains double quotes.

Template strings (enclosed within backticks) should not be used for string literals that do not actually contain any interpolation.

const myString: string = "You say \"goodbye\" and I say \"hello\".";

3.6.2. Use concatenation when writing long string literals

String literals that are too long to fit on a single line should be spread across multiple lines by concatenating many shorter string literals.

const myLongStringLiteral = (
    "This is a long string literal. In fact, it is so long, that it could " +
    "not possibly fit on a single line and still be readable. " +
    `Remember that the recommended maximum line length is ${MaxLineLength} ` +
    "characters!"
);

4. Comments

The following guidelines mainly pertain to what is expected of code comments.

4.1. Begin all comments with a single space

There should be a single space character in between a comment's opening slashes // and the first character of the comment's text.

// Note the space at the beginning of this comment!

4.2. Prefer multiple single-line comments to multi-line block comments

Long comments should be spread across multiple single line comments (i.e. // comment) rather than given in a single multi-line block comment (i.e. /* comment */).

// This is a longer comment providing a lot of information about a function.
// It describes in detail the function's purpose, its accepted input, and its
// expected output. It's far too much information to fit on just one line.
function myFunction(value: number): number {
    return doStuffWith(value);
}

4.3. Prefer comments on their own line to trailing comments

Comments should usually not be on the same line as code. Comments should be on their own line, preceding the code that they apply to.

// [Comment explaining the purpose of my variable]
let myVariable: number = 0;

4.4. Trailing comments should have one space between "//" and the end of the line

Note that 4.3. discourages the use of trailing comments. When trailing comments are used, there should always be exactly one space in between the last character of code and the first character / of the trailing comment.

doStuff(); // My trailing comment

4.5. Documenting comments

The following guidelines mainly pertain to when documenting comments are encouraged or required.

4.5.1. Document all classes and interfaces

Every class and interface should have a comment explaining its purpose, even if it is only repeating information that should be self-evident.

// This class stores the color channel information taken from a PLAYPAL
// lump. Note that the color data is 24-bit. Color channel values should
// always be in the range [0, 256] inclusive.
class MyColorClass {
    // The color's 8-bit red color channel.
    red: number;
    // The color's 8-bit green color channel.
    green: number;
    // The color's 8-bit blue color channel.
    blue: number;
}

4.5.2. Document all functions and methods

Every function or method should have a comment explaining its purpose, even if it is only repeating information that should be self-evident.

// Log a friendly greeting to the console.
function sayHello(): void {
    console.log("Hello, world!");
}

4.5.3. Document the impure behavior of functions

Functions and methods with impure behavior should have that impure behavior documented in comments to the greatest extent that is practical.

Note that impure behavior includes modifying a function's inputs or reading or writing global state.

// [Comment explaining the purpose of my function]
// Calling this function will cause `myPreviouslyDeclaredGlobal`
// to be modified.
function myFunctionWithSideEffects(): void {
    myPreviouslyDeclaredGlobal = doStuff();
}

4.5.4. Document all constants and enumerations

Constants - defined as values set once and never reassigned, not necessarily any variable declared using const - should always be accompanied by a comment explaining their purpose and their value.

// The Doom engine palette lump is always named "PLAYPAL".
const PlaypalLumpName: string = "PLAYPAL";

Enumerations should be preceded by a documenting comment, and every member should have a comment explaining its purpose.

// Enumeration of Doom linedef flags which pertain to texturing.
enum LinedefTextureFlags {
    // Unpegged upper texture
    UpperUnpegged = 0x0008,
    // Unpegged lower texture
    LowerUnpegged = 0x0010,
}

4.5.6. Document variables or attributes that are not completely self-explanatory

It is not necessary to write a comment explaining every variable or attribute, but those whose function is not immediately obvious from looking at the declaration should be accompanied by documentation comments.

4.5.6. Document statements or expressions that are not completely self-explanatory

It is not necessary, and in fact discouraged, to write a comment explaining every statement and expression. However, those statements and expressions which are more complicated or with less obvious purpose should be accompanied by documentation comments.

// Do stuff with the length of the vector described by (x, y).
doStuffWith(Math.sqrt((x * x) + (y * y)));

4.6. Todos

The following guidelines mainly pertain to how "TODO" comments should be used.

4.6.1. Todo comments should begin with "// TODO:"

Comments recording future or unfinished tasks should consistently begin with the characters // TODO: .

When a todo comment is too long to fit on a single line, only the first line explaining the task should be preceded by TODO.

function myScaffoldingFunction(): void {
    // TODO: Implement this function
}

4.6.2. Todo comments should include an explanation of the unfinished task

It is not acceptable to write a todo comment without including some written explanation of the task that is not finished. Do not write a comment that says // TODO without being followed by more text explaining why the comment is present.

4.6.3. Todo comments should include the URL for a relevant issue or PR

Where practical, todo comments should refer to an issue in the issue tracker, or to a relevant pull request. In general, if such a relevant issue or other link does not exist, then it should be created and referenced in the todo comment before the comment is included in the master branch of the code repository

// TODO: Create a help page and assign the URL.
// See https://github.com/pineapplemachine/jsdoom/issues/123456
const myHelpUrl: string = "";

5. Names

The following guidelines mainly pertain to the naming of variables, classes, etc. and source file names.

5.1. Use descriptive names

All classes, functions, variables etc. should be assigned descriptive names that make their purpose as clear as possible. Avoid using vague or generic names that do not communicate purpose.

5.2. Do not use needless or unconventional abbreviations

Identifiers should generally not contain abbreviations unless those abbreviations are essentially universal and/or the text that they are abbreviating is impractically long to actually include in code.

"HTML" abbreviating "HypertextMarkupLanguage" or "WAD" abbreviating "WheresAllTheData" is good and encouraged. However, "Idx" abbreviating "Index" or "Err" abbreviating "Error" is not acceptable. Please use common sense in judging whether an abbreviation is really necessary, and whether it might make the code more difficult to read.

5.3. Names should contain only "A"-"Z", "a"-"z", and "0"-"9"

Identifiers should be made up only of the characters A through Z, a through z, and 0 through 9. Identifiers should not have an underscore _ in their name, even if they identify private members.

Do not use characters such as emoji or math symbols in variable names.

const myAsciiVariableName: number = 0;

5.4. Classes and constants should have PascalCase names

Class and interface names names, constructors, enumerations, and the names of constants should be written in PascalCase.

Note that constant in this case does not mean everything declared using const, but rather it refers to any variable that is initialized once and never changed during program execution. Constants are not a matter of syntax in TypeScript, but a matter of intent.

5.5. Functions and variables should have camelCase names

Names of functions, methods, variables, attributes, function parameters, and anything else that is not mentioned by 5.4. should be written using camelCase.

5.6. TypeScript source files should have camelCase names

TypeScript source files and directories containing TypeScript source files should be assigned pascalCase names.

- file.ts
- fileList.ts
- fileType.ts

5.7. Single-character names

The following guidelines mainly pertain to the use of single-character names. Single-character names are mostly discouraged, but are still the most appropriate naming choice for some cases.

5.7.1. Single-character names should not be used except as mentioned in 5.7.

Except as otherwise mentioned below in 5.7., single-character names should not normally be used.

Here is a rule of thumb: Single-character identifiers should only be used when it follows a long-standing mathematical or programming convention, such that choosing any other name may make the purpose of the variable, etc. less clear.

5.7.2. Parameters with very clear purpose may be named "a", "b", "c", and so on

Sequential single-letter identifiers such as a, b, c, and so on are acceptable where they are used to name the parameters of a function with very clear inputs and purpose, where those inputs are mainly distinguished by the order in which they appear.

// `a` and `b` are the preferred parameter names for comparator functions.
myArray.sort((a: number, b: number) => {
    return a - b;
});

5.7.3. The interpolant parameter should be named "t"

Where a parameter or other named value is used as the interpolant parameter of an interpolation function, that value should be named t.

// Linearly interpolate between the numbers `a` and `b`.
// The value `t` should normally be in the inclusive range [0.0, 1.0].
function lerp(a: number, b: number, t: number): Vector {
    return (a * (1 - t)) + (b * t);
}

5.7.4. Extremely generic type parameters may be named "T"

A generic function or class accepting a single type parameter may have that parameter named T if the purpose of the parameter is generic enough to not lend itself to a more descriptive name.

5.7.5. Coordinates should be named "x", "y", "z", and "w"

The single-character names x, y, z, and w are acceptable and encouraged when they refer to spatial coordinates along the corresponding axes.

5.7.6. Vector components should be named "ijk" or "xyzw"

The components of vectors or quaternions should normally be named either i, j, k or x, y, z, w.

// [Comment explaining the purpose of my three-dimensional vector class]
class MyVector {
    x: number;
    y: number;
    z: number;
}

6. Syntax conventions

The following guidelines mainly pertain to what TypeScript syntax options should be favored or avoided.

6.1. Do not use "eval" or the function constructor

Do not use the eval built-in function. Do not use the Function constructor to create a new function. This functionality is not efficient and it opens the door to security issues.

6.2. Do not use "var"

Do not use var when declaring variables. Use let or const instead.

const myVariable: number = 0;

6.3. Prefer "const" to "let" when declaring variables

Use const instead of let whenever it is syntactically valid, i.e. when the declared reference is never changed.

6.4. Do not use undeclared variables

Do not use variables without declaring them. Do not reference variables that have not yet been declared at the time that the code will be executed.

6.5. Use seperate declarations

Do not declare multiple variables together on the same line. Use separate declarations, with each declaration on its own line.

const myFirstNumber: number = 0;
const mySecondNumber: number = 0;

6.6. Prefer "if" and "else if" over "switch" and "case"

Prefer using a series of if and else if statements instead of using a switch statement.

if(myVariable === 0){
    doStuff();
}else if(myVariable === 1){
    doOtherStuff();
}else if(myVariable === 2){
    doYetMoreStuff();
}else{
    doDefaultStuff();
}

6.7. Do not nest ternary expressions

Ternary expressions a ? b : c should not be nested.

6.8. Prefer strict equality over regular equality

The strict equality === and strict inequalty !== operators should be used instead of the regular equality == and inequality != operators.

const myComparison: boolean = (myFirstValue === mySecondValue);

6.9. Prefer "as" when writing type assertions

When writing a type assertion, prefer syntax like value as Type over syntax like <Type> value. (Why?)

const myValue: number = myOtherValue as number;

6.10. Prefer Type[] to Array

When describing an array type, prefer syntax like Type[] to syntax like Array<Type>.

const myStringArray: string[] = ["Hello", "World"];

6.11. Do not refer to the "arguments" object

Functions should not refer to the arguments object.

6.12. Prefer rest parameters over referring to the "arguments" object

Functions which accept a variable number of arguments should do so using a rest parameter ... and not by accessing the arguments object.

// [Comment explaining the purpose of my function]
function myVariadicFunction(...strings: string[]): void {
    doStuffWithList(strings);
}

6.13. Avoid the spread operator in array and object literals

Prefer using functions like Array.concat or Object.assign over using the spread operator to construct arrays or objects.

// Use `Array.concat` instead of the spread operator
const myConcatenatedArray = (
    myFirstArray.concat(mySecondArray)
);
// Assign each key individually or, if the keys are numerous or not
// necessarily known ahead of time, use `Object.assign` instead of
// using the spread operator.
const myComposedObject = {};
Object.assign(myComposedObject, myFirstObject, mySecondObject);

6.14. Do not mix full object properties with shorthand properties

An object should either use only shorthand properties or only full properties. A single object literal should not mix both types of properties.

const myObjectWithFullProperties: Object = {
    firstValue: firstValue,
    secondValue: secondValue,
};
const myObjectWithShortProperties: Object = {
    firstValue,
    secondValue,
};

6.15. Imports

The following guidelines mainly pertain to how imports should be written.

6.15.1. Imports belong at the beginning of a source file

Import statements should be at the very top of a TypeScript source file. They should not appear anywhere else.

6.15.2. Prefer selective imports over default imports

Prefer selective imports (imports with braces {}) over default imports (imports without braces).

import {WADFile} from "@src/wad/file";

However, modules should still have a default export where it makes sense to choose a default.

// Actual export from "@src/wad/file"
export default WADFile;

6.15.3. Prefer absolute over relative imports

Absolute import paths (those starting with an "at" sign @) should be preferred over relative paths (those starting with a dot .). Note that the meaning of these absolute paths are defined by macros in the TypeScript config file tsconfig.json and the webpack config file webpack.config.js.

import {WADFile} from "@src/wad/file";

6.15.4. Group and order imports logically, then alphabetically by filename

Imports should first be ordered by logical group, and then alphabetically by filename. Groups of similar imports should be ordered from most general to most specialized. Here is a guideline for how to separate and order these groups:

  1. Native dependencies, such as Node.js imports.
  2. External dependencies, i.e. those listed in package.json.
  3. Jsdoom library dependencies, e.g. @src/wad/ or @src/lumps/.
  4. Jsdoom UI or engine dependencies, e.g. @web/.
import * as fs from "fs";
import * as path from "path";

import * as UPNG from "upng-js";

import {WADFile} from "@src/wad/file";
import {WADFileList} from "@src/wad/fileList";

import {LumpTypeView} from "@web/lumpTypeView";

6.15.5. Do not include unused imports

Avoid importing unused symbols or modules.

6.15.6. Avoid importing modules for side-effects only

Avoid writing side-effect-only import statements, i.e. statements such as import "module";.

6.16. Ordering of class members

The following guidelines mainly pertain to how class member declarations should be ordered.

6.16.1. Declare static attributes first

Static attributes and constants should come before all other declarations in a class.

class MyClass {
    // [Comment explaining the purpose of my constant]
    static readonly MyStaticConstant: number = 0x8000;
}

6.16.2. Declare instance attributes second

Instance attributes should be declared after static attributes but before the constructor and any member functions or methods.

class MyClass {
    // [Comment explaining the purpose of my attribute]
    myFirstAttribute: string;
    // [Comment explaining the purpose of my attribute]
    mySecondAttribute: string;
}

6.16.3. Declare the constructor third

The class constructor should come after all static and instance attribute declarations but before all method and member function declarations.

class MyClass {
    constructor() {
        this.myFirstAttribute = "hello";
        this.mySecondAttribute = "world";
    }
}

6.16.4. Declare static member functions fourth

Static member functions should appear after attribute declarations and after the class constructor, but before instance method declarations.

class MyClass {
    // [Comment explaining the purpose of my function]
    static myStaticFunction(): MyClass {
        return new MyClass();
    }
}

6.16.5. Declare instance methods last

Instance methods should appear after all other declarations in a class.

class MyClass {
    // [Comment explaining the purpose of my method]
    getTotalLength(): number {
        return this.myFirstAttribute.length + this.mySecondAttribute.length;
    }
}

7. Program logic conventions

The following guidelines mainly pertain to the use of consistent logic and design conventions in order to keep code modular and maintainable.

7.1. Prefer local state over global state

Code which relies on local state is normally easier to understand and maintain than code which relies on global state. It is not possible to completely avoid global state, particularly in code that deals with the DOM. However, global state should be avoided when possible. Whenever possible, state should be stored locally in scoped variables or in class instances.

7.2. Prefer immutable state over mutable state

Where it will not substantially impact performance in a negative way, treat variables and objects as though they were immutable.

7.3. Prefer pure functions over impure functions

Pure functions are those which accept inputs and produce their output without making any changes to the inputs and without producing any side-effects. In essence, a pure function is one which can be called at any time, any number of times, and the same inputs will always produce the same output.

// [Comment explaining the purpose of my pure function]
function myPureFunction(value: number): number {
    return value + value;
}

7.4. Functions should not modify their inputs

Avoid writing functions that modify their inputs. Ensure that where such functions do exist, their modification of the input is clearly documented.

7.5. Class getters should treat the instance as logically const

Getter methods (i.e. methods preceded by the get keyword) should treat the instance this as logically const. Logical const means that the object may technically be modified, but any modification that does take place should not affect or be visible to code which uses only the object's intended and documented API.

class MyClass {
    // Used to cache the result of the myExpensiveProperty getter.
    private lazilyComputedValue: number = NaN;
    
    // No modification of the class instance whatsoever.
    // `this` is literally const.
    get myProperty(): number {
        return 0x0100;
    }
    
    // Modification is trivial to the outside observer.
    // `this` is logically const.
    get myExpensiveProperty(): number {
        if(Number.isNaN(this.lazilyComputedValue)){
            this.lazilyComputedValue = doExpensiveComputation();
        }
        return this.lazilyComputedValue;
    }
}

7.6. Prefer template strings over concatenation

Prefer using template strings over string concatenation or joining.

const myString: string = `${helloString} ${worldString}!`;

7.7. Prefer joining over repeated concatenation

Strings that are incrementally assembled, e.g. in a loop, should have their substrings pushed to an array and combined at the end of the loop with a single join statement. Avoid assembling strings via repeated concatenation.

// Create a comma-separated list representing the vertexes in an array.
function vertexListToString(vertexes: Vertex[]): string {
    const parts: string[] = [];
    for(const vertex of vertexes){
        parts.push(`(${vertex.x}, ${vertex.y})`);
    }
    return parts.join(", ");
}