iModel.js TypeScript Coding Guidelines
These are the TypeScript coding guidelines that we expect all iModel.js contributors to follow. Where possible, these guidelines are enforced through our TSLint configuration file (tslint.json).
Names
- Use PascalCase for type names.
- Do not use
I
as a prefix for interface names. - Use PascalCase for enum values.
- Use camelCase for function names.
- Use camelCase for property names and local variables.
- Use
_
as a prefix for private properties. - Use whole words in names when possible. Only use abbreviations where their use is common and obvious.
- We use "Id", "3d", "2d" rather than capital D.
Files
- Use the .ts file extension for TypeScript files
- TypeScript file names should be PascalCase
Types
- Do not export types/functions unless you need to share it across multiple components.
- Do not introduce new types/values to the global namespace.
- Within a file, type definitions should come first.
Do not use null
- Use
undefined
. Do not usenull
except where external libraries require it.
===
and !==
Operators
- Use
===
and!==
operators whenever possible. - The
==
and!=
operators do type coercion, which is both inefficient and can lead to unexpected behavior.
Strings
- Use double quotes for strings.
General Constructs
- Always use semicolons. JavaScript does not require a semicolon when it thinks it can safely infer its existence. Not using a semicolon is confusing and error prone. Our TSLint rules enforce this.
- Use curly braces
{}
instead ofnew Object()
. - Use brackets
[]
instead ofnew Array()
.
Preserve vertical screen space
Programmer monitors are almost always wider than they are tall. It is common for widths to be at least 120 columns but heights to be less than 100. Therefore to make the greatest use of screen real estate, it is desireable to preserve vertical screen space wherever possible.
- Some codebases advocate breaking lines at 80 columns. With current screen sizes, this is silly and wasteful. Don't break lines before 120 columns.
- Don't use blank lines unnecessarily. For example the first line of a function not should be a blank line.
- There should never be more than one blank line in a row.
- Don't use clever/pretty multi-line comment blocks to separate sections of code. One line suffices, if you absolutely feel the need to include them. Usually they aren't necessary. Your well written, accurate and complete documentation and logical source organization is all the help anyone needs to understand your code.
- If a function has only a single statement, it should be on one line.
// No !!!
public middle(): number {
return this.minimum + ((this.maximum - this.minimum) / 2.0);
}
// Correct (1 line vs 3) !!!
public middle(): number { return this.minimum + ((this.maximum - this.minimum) / 2.0); }
Style
- Use arrow functions over anonymous function expressions.
- Open curly braces always go on the same line as whatever necessitates them.
- Never use
var
. Instead useconst
where possible and otherwise uselet
. - Use a single declaration per variable statement (i.e. use
let x = 1; let y = 2;
overlet x = 1, y = 2;
). - Parenthesized constructs should have no surrounding whitespace. A single space follows commas, colons, semicolons, and operators in those constructs. For example:
for (let i = 0, n = str.length; i < 10; ++i) { }
if (x < 10) { }
public calculate(x: number, y: string): void { . . . }
- Use 2 spaces per indentation. Do not use tabs!
- Turn on
tslint
in your editor to see violations of these rules immediately.
Return
If a return statement has a value you should not use parenthesis () around the value.
return ("Hello World!"); // bad
return "Hello World!"; // good
Certain schools of programming advice hold that every method should have only one return statement. This could not be more misguided. Always return as soon as you know there's no reason to proceed.
// bad!!
public getFirstUser(): Person | undefined {
let firstUser?: Person;
if (this.hasUsers()) {
if (!this.usersValidated()) {
if (this.validateUsers())
firstUser = this.getUser(0);
} else {
firstUser = this.getUser(0);
}
}
return firstUser;
}
// ok!!
public getFirstUser(): Person | undefined {
if (!this.hasUsers())
return undefined;
if (!this.usersValidated() && !this.validateUsers())
return undefined;
return return this.getUser(0);
}
// best!!! For a simple case like this that is deciding between 2 return values
public getFirstUser(): Person | undefined {
const userInvalid = !this.hasUsers() || (!this.usersValidated() && !this.validateUsers());
return userInvalid ? undefined : this.getUser(0);
}
Always explicitly define a return type for methods that are more than one line. This can help TypeScript validate that you are always returning something that matches the correct type.
// bad!! No return type specified
public getOwner(name: string) {
if (this.isReady)
return this.widget.getOwner(); // is this a Person???
...
return new Person(name);
}
// good!!
public getOwner(name: string): Person {
if (this.isReady)
return this.widget.getOwner(); // if this is not a Person, compile error
...
return new Person(name);
}
for methods that are one line and for which the return type is obvious, it is not necessary to include the return type:
// fine, return type is obvious
public getCorner() { return new Point3d(this.x, this.y); }
When calling methods, the best practice would be to explicitly specify the return type. In the case of async methods calls, these calls almost always involve awaiting the results, and this practice guards against omitting the await keyword - a frequent cause of hard to debug race conditions.
// bad!! no return type specified for async call, and missing await is not caught by the compiler
const iModels = iModelHub.getIModels(projectId);
// good!! omitting the await would be caught by the compiler as a type mismatch
const iModel: IModel[] = await iModelHub.getIModels(projectId);
Getters and Setters
A common pattern is to have a private
member that is read/write privately within the class, but read-only to the public API. This can be a good use for a getter. For example:
class Person {
private _name: string;
public get name(): string { return _name; } // read-only to public API, so no setter provided
}
Note, however, if the value supplied by the getter is established in the constructor and can never be changed, the following may be preferable:
class Person {
constructor(public readonly name: string) { }
}
Another valid use of getters and setters is when you want to give the appearance of having a public member but you don't actually store the data that way. For example:
class PersonName {
constructor (public firstName: string, public lastName: string) { }
public get fullName(): string { return this.firstName + " " + this.lastName; }
public set fullName(name: string): void {
const names: string[] = name.split(" ");
this.firstName = names[0] || "";
this.lastName = names[1] || "";
}
}
It is also good to use getters and setters if data validation is required (which isn't possible in the case of a direct assignment to a public member).
There are cases where getters and setters would be overkill. For example:
// This is fine!
class Corner {
constructor(public x: number, public y: number, public z: number) { }
}
use "?:" syntax vs. " | undefined"
When declaring member variables or function arguments, use the TypeScript "?:" syntax vs. adding " | undefined" for variables that can be undefined. For example
class Role {
public name: string;
public description: string | undefined; // Wrong !!
}
class Role {
public name: string;
public description?: string; // Correct !!
}
Prefer getters where possible
If a public method takes no parameters and its name begins with a keyword such as "is", "has", or "want", the method should be a getter (specified by using the "get" modifier in front of the method name). This way the method is accessed as a property rather than as a function. This avoids confusion over whether it is necessary to include parenthesis to access the value, and the caller will get a compile error if they are included. This rule is enforced by TsLint.
If the value being returned is expensive to compute, consider using a different name to reflect this. Possible prefixes are "compute" or "calculate", etc.
Don't repeat type names unnecessarily
TypeScript is all about adding types to JavaScript. However, the compiler automatically infers type by context, and it is therefore not necessary to decorate every member or variable declaration with its type, if it is obvious. That only adds clutter and obscures the real code. For example,
let width: number = 7.3; // useless type declaration
public isReady: boolean = false; // useless type declaration
public readonly origin: Point3d = new Point3d(); // useless type declaration
let width = 7.3; // correct
public isReady = false; // correct
public readonly origin = new Point3d(); // correct
const upVector: Vector3d = rMatrix.getRow(1); // good, helps readability. Not strictly necessary.
However, as stated above, it is a good idea to always include the return type of a function if it is more than one line, to make sure no return path has an unexpected type.
Error Handling
For public-facing APIs we have decided to prefer exceptions (throw new Error
) and rejecting promises (Promise.reject
) over returning status codes. The reasons include:
- Exceptions can keep your code clean of if error status then return clutter. For example, a series of API calls could each be affected by network outages but the error handling would be the same regardless of which call failed.
- Exceptions let you return the natural return value of success rather than an unnatural composite object.
- Exceptions can carry more information than a status return.
- Status returns can be ignored, but exceptions can't. If the immediate layer does not handle the exception it will be bubbled up to the outer layer.
- The optional
message
property of anError
should (if defined) hold an English debugging message that is not meant to be localized. Instead, applications should catch errors and then separately provide a context-appropriate localized message.
Note: Returning
SomeType
and throwing anError
is generally preferred over returningSomeType | undefined
.
Asynchronous Programming
- Use
Promise
- Use
return Promise.reject(new MyError())
rather than resolving to an error status. The object passed intoPromise.reject
should be a subclass ofError
. It is easy to forget thereturn
so be careful. - Prefer
async
/await
over.then()
constructs
Reference Documentation Comments
We have standardized on TypeDoc for generating reference documentation.
TypeDoc runs the TypeScript compiler and extracts type information from the generated compiler symbols. Therefore, TypeScript-specific elements like classes, enumerations, property types, and access modifiers will be automatically detected. All comments are parsed as markdown. Additionally, you can link to other classes, members, or functions using double square brackets.
The following JavaDoc tags are supported by TypeDoc:
@param
- The parameter name and type are automatically propagated into the generated documentation, so
@param
should only be included when more of a description is necessary. - Use plain
@param
. Do not use@param[in]
or@param[out]
as this confuses TypeDoc. - The parameter description should start with a capital letter.
- The parameter name and type are automatically propagated into the generated documentation, so
@returns
- The return type is automatically propagated into the generated documentation, so
@returns
should only be included when more of a description is necessary. - The
@returns
description (when provided) should start with Returns for readability within the generated documentation. - The
@return
JavaDoc tag is also supported, but@returns
is preferred for readability and consistency with@throws
. <!-- - TODO:
- Need to decide how to document methods returning
Promise<T>
. Should the description mention aPromise
or justT
since the return type will clearly indicatePromise
and usingawait
will causeT
to be returned.
- Need to decide how to document methods returning
- ->
- The return type is automatically propagated into the generated documentation, so
@throws
- If a method can potentially throw an
Error
, it should be documented with@throws
as there is no automated way that thrown errors make it into the generated documentation. - There can be multiple
@throws
lines (one for each differentError
class) in a method comment. - A link to the
Error
class should be incorporated into the description.
- If a method can potentially throw an
@internal
- TypeDoc will not document the class, method, or member. This is useful for internal-only utility methods that must be public, but should not be called directly by outside API users.
See below for the recommended format of documentation comments:
/** This is a valid single-line comment. */
public myMethod1(): void { }
/**
* This is a valid multi-line comment.
* The description uses double brackets to link to [[NameOfRelatedClass]].
* The description also links to [[myMethod1]].
* @param param1 Description of parameter
* @returns Returns the number associated with param1.
* @throws [[NameOfErrorClass]] when the parameter is invalid.
*/
public myMethod2(param1: string): number { /* ... */ }
Defining JSON 'Wire Formats'
A common pattern in JavaScript is to transfer information from one context to another by serializing/deserializing to strings.
For example:
- from a backend program to a frontend program, or vice-versa
- from C++ to JavaScript, or vice-versa
- saving object state to/from a persistent store, such as a database
Since JSON strings are often sent over an internet connection, these strings are commonly referred to as "wire formats".
This pattern is built-in to JavaScript via JSON, using the methods JSON.stringify and JSON.parse. However, those methods are defined to take/generate objects of type any
. That gives callers no help interpreting the contents of the JSON.parse
result or supplying the correct input to JSON.stringify
. Fortunately TypeScript has very nice techniques for defining the shape of an object, via Interfaces and Type Aliases.
Our convention is to define either a Type Alias
or an interface
with the suffix Props
(for properties) for any information that can be serialized to/from JSON. There will often be an eponymous class without the Props
suffice to supply methods for working with instances of that type. A serializeable class Abc
will usually implement AbcProps
, if it is an interface
. Then, either its constructor or a static fomJson
method will take an AbcProps
as its argument, and it will override the toJSON
method to return an AbcProps
. Anyone implementing the "other end" of a JSON serialized type will then know what properties to expect/include.
For example, in @bentley/geometry-core
we have a class called Angle
. You will find code similar to:
/** The Properties for a JSON representation of an Angle.
* If value is a number, it is in *degrees*.
* If value is an object, it can have either degrees or radians.
*/
export type AngleProps = number | { degrees: number } | { radians: number };
export class Angle {
. . .
public static fromJSON(json?: AngleProps): Angle {. . .}
public toJSON(asRadians?: boolean): AngleProps { return asRadians ? { radians: this.radians } : { degrees: this.degrees }; }
}
From this we can tell that an Angle may be serialized to/from JSON as either:
- a
number
, in which case it will be a value in degrees - an object that:
- has a member named
degrees
of typenumber
, or - has a member named
radians
of typenumber
.
Likewise, in @bentley/geometry-core
, we have a class called XYZ. This is a base class for 3d points and vectors. We define the following type:
/** Properties for a JSON XYZ.
* If an array, its values are [x,y,z].
* @note Any undefined values are 0.
*/
export type XYZProps = { x?: number; y?: number; z?: number } | number[];
That tells you that a value of type XYZ (either Point3d or a Vector3d) may be serialized to/from JSON as:
- an object with optional members
x
,y
, andz
of typenumber
, or - an array of
numbers
, in the order x, y, z. - Any
undefined
values will be 0.
A correctly implemented program that interprets a JSON string containing an XYZ value must handle both forms (object and array). However, it is free to choose either form for creating JSON strings from XYZ values.
Copyright notice
Every .ts file should have this notice as its first lines:
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
verbatim, with the xx replaced with the current year.
Source Code Editor
While not an absolute requirement, we recommend and optimize for Visual Studio Code. You will be likely be less productive if you attempt to use anything else. We recommend configuring the TSLint extension for Visual Studio Code and using our tslint.json to get real-time feedback.
Last Updated: 05 June, 2020