Learning > guidelines > typescript coding guidelines Edit this page 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 use null 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 of new Object(). Use brackets [] instead of new 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 use const where possible and otherwise use let. Use a single declaration per variable statement (i.e. use let x = 1; let y = 2; over let 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 an Error 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 an Error is generally preferred over returning SomeType | undefined. Asynchronous Programming Use Promise Use return Promise.reject(new MyError()) rather than resolving to an error status. The object passed into Promise.reject should be a subclass of Error. It is easy to forget the return 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. @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. Last Updated: 13 June, 2024