These guidelines and principles shall provide a common basis for coding Coaty framework components and application projects using TypeScript.
The coding guidelines are obligatory for TypeScript developers that contribute to the Coaty framework. We strongly suggest that application developers shall also follow these guidelines in their Coaty projects.
If you are new to TypeScript programming, we recommend to take a look at the official TypeScript website. Its “Playground” is especially useful to interactively try some TypeScript code in your browser.
To program the Coaty framework, we recommend to use Visual Studio Code, a free, open source IDE that runs everywhere. Install the VS Code extension “TypeScript TSLint Plugin” to enable TypeScript linting within the IDE.
The Coaty framework uses TSLint
for linting the TypeScript code. The TS linter is
run automatically when building the framework distribution package with npm run build
.
To build distribution packages but not lint the source code use npm run build:nolint
.
You can also invoke the linter on its own: npm run lint
.
To fix linting errors automatically, invoke npm run lint:fix
.
Please ensure that the TypeScript files compile correctly before running the linter.
The linter uses a recommended set of rules that conform with the TypeScript Coding
Guidelines for Coaty projects as described in the following section. These rules
are defined in the TSLint configuration file tslint-config.json
which is part of
the framework’s distribution package, so a Coaty application project can also use them
in its tsconfig.json
file:
{
"extends": "coaty/tslint-config",
"rules": {
// You may overwrite some rule settings here
}
}
All available rules and options for TSLint are described here.
You may enable/disable TSLint or a subset of rules within certain lines of a TypeScript file with the following comment rule flags
/* tslint:disable-next-line:rule1 rule2 rule3... */
/* tslint:disable:rule1 rule2 rule3... */
...
/* tslint:enable:rule1 rule2 rule3... */
isVal()
, hasVal()
, shouldVal()
, or canVal()
._
for private properties and functions to distinguish their usage
easily from public or protected ones in TS code or transpiled JS code._
for protected or public properties and functions.I
as a prefix for interface names that define objects.I
as a prefix for interface names that define call and property
signatures in the classic sense as used in Java or C#.for (var i = 0, n = str.length; i < 10; i++) { }
if (x < 10) { }
function f(x: number, y: string): void { }
else
goes on the same line as the if
block’s closing curly brace.catch
goes on the same line as the try
block’s closing curly brace.undefined
, do not use null
. typeof null
returns "object"
which
incorrectly suggests that null
is an object (it isn’t, it’s a primitive value).
This is an immanent bug in JavaScript language design.===
and !==
.==
and !=
.const
, let
) references wherever possible.var
).const
for all your references that should be immutable.let
only if you must mutate a reference (e.g. a counter variable).const
or let
declaration per variable.const
s and then group all your let
s.var x = 1; var y = 2;
over var x = 1, y = 2;
).""
for literal strings in TypeScript files. Double quotes
are preferred over single quotes because JSON uses double quotes; double
quotes are broadly used in programming; and double quotes look clearly different
than backticks.`${firstName} ${lastName}`
const { firstname, lastname } = user;
const [first, , third] = array;
...
to copy arrays: [...array]
arguments
, opt to use rest syntax ...
instead.function(opts = {}) {}
this
, which is
usually what you want.(x) => x + x
is bad but the following are good:
x => x + x
(x,y) => x + y
<T>(x: T, y: T) => x === y
return
statement.class
. Avoid manipulating prototype
directly.class extends
for inheritance.this
to help with method chaining.toString()
method, just make sure it works
successfully and causes no side effects.Number
, String
, Boolean
, or Object
. These types
refer to non-primitive boxed objects that are almost never used appropriately in
JavaScript code.number
, string
, and boolean
Object
, use the non-primitive object
type (added in TypeScript 2.2).T
for the type variable if only one is needed.identify("myString");
instead of identify<string>("myString");
.function create<T>(thing: {new(): T;}): T { return new thing(); }
.import
, export
) over a non-standard
module system.import { Foo } from "module";
instead of
import Foo = require("module");
.import { Task } from "@coaty/core";
instead of import { Task } from "@coaty/core/model/task";
.index.ts
), not from an individual TS file.import { map, take } from "rxjs/operators";
class FooBar
should be
foo-bar.ts
.TaskRequestComponent
should be task-request.component.ts
./** ... */
. Include a description, specify values
for all parameters (@param
) and return values (@returns
).
Don’t specify explicit type information for parameters as it can be deduced from source code.//
for single line comments. Place single line comments on a newline
above the subject of the comment. Put an empty line before the comment.@fixme
or @todo
or @hack
helps other
developers quickly understand if you’re pointing out a problem that needs to
be revisited, or if you are suggesting a solution to the problem that needs
to be implemented, or if you are explaining the reason for a workaround.
These are different than regular comments because they are actionable.
The actions are @fixme -- need to figure this out
or
@todo -- need to implement
or @hack -- reason for
. You can also
append the initials of the person who is responsible: @todo(HHo)
.declare module "<name>"
statement and that exports are properly defined. Old-style typings often miss this
convention so that the TypeScript compiler complains with a Cannot find module
error.Framework developers who are implementing new framework components must strictly follow the coding principles listed below:
Asynchronous date flow between components should be modelled using RxJS observables. An introduction to Reactive Programming can be found here. Examples and explanations can be found on the RxJS and Learn RxJS websites.
In RxJS, use pipeable operator syntax. Import only those RxJS operators that
you actually use in your code, e.g. import { map, take } from
"rxjs/operators";
. Do not load the whole operator bundle, it consumes a lot
of memory.
Whenever you subscribe to an RxJS observable in your code explicitely, you
should consider to keep the subscription in order to unsubscribe when the
observable is no longer used to avoid memory leaks. For example, in an Angular
or Ionic view component you should unsubscribe at the latest when the
component is destroyed by implementing the OnDestroy
interface or the Ionic
ionViewWillUnload
method. As an alternative you can use observables in
Angular data binding expressions in combination with the async
pipe which
subscribes and unsubscribes automatically.
Prefer implicit, imperative complete notifications over explicitly
unsubscribing your RxJS observables. Complete subjects and observables if no
longer used so that subscriptions are automatically unsubscribed to avoid
memory leaks. Operators that complete implicitely include take
, takeUntil
,
etc.
Avoid using the RxJS share
operator in combination with source observables
that are realized as behavior subjects. Only the first subscriber of the
shared observable will be pushed the initial value of the behavior subject
immediately when subscribed. Any subsequent subscribers will not get the
initial value replayed when subscribed but only the next values which the
subject emits lateron. That is because the share
operator returns a
multicast (hot) observable.
The unified persistent storage and retrieval API of the framework is based on promises. When using promise-based APIs to invoke multiple asynchronous operations in sequence, it is best practice to use composing promises and promise chaining. Note that after a catch operation the promise chain is restored. Avoid the common bad practice pattern of nesting promises as if they were callbacks.
If a component encounters a transient error this error must be caught and handled internally. The component must not throw transient errors to callers. You may decide to log errors and report them to dependent components using e.g. an error emitting observable, a rejected promise, or an error callback.
You should throw an error in the component’s constructor if the passed in arguments are invalid or the component cannot start its lifecycle with the provided configuration parameters.
Note that the onDispose
method of container components is only called
when the container is shut down by invoking the shutdown
method. You should
not perform important side effects in onDispose
, such as saving application
state persistently, because the onDispose
method is not guaranteed to be called
by the framework in every circumstance, e.g., when the application process
terminates abnormally or when the shutdown
method is not called by your app.
Coaty framework objects are defined as interfaces, not classes, deriving from
CoatyObject
. The reason is that Objects are transferred as JSON event data
between distributed system components. Define your own custom object type
extending CoatyObject
or one of its subinterfaces. Specify a unique canonical
object type name in the property objectType
. Do not use class instances
as property values of your custom object types because class instances cannot
be serialized correctly, as they are serialized as pure JavaScript objects
without prototype information.
Whenever you add new core object types to the framework, ensure that the
new interface type is added to the CoreTypes
class and the CoreType
type
definition (see model/types.ts
).
Whenever an object ID of a CoatyObject
is hardcoded (e.g. in a config file), use
a Version 4 UUID generator to ensure the generated UUID is unique.
An online generator can be found here.
Ensure that in the string representation of a UUID the hexadecimal values “a”
through “f” are output as lower case characters. Never use UUIDs which contain
uppercase characters because external components such as databases could
automatically convert such a UUID (used e.g. as a primary key) to lowercase
which results in mismatches on retrieval.
Copyright (c) 2018 Siemens AG. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.