TypeScript
We use Prettier and ESLint to automatically format
and lint the code base. npm ci
will automatically install pre-commit hooks to run Prettier and
ESLint on your changes each time you create a commit.
Alternatively, you can run them manually:
npm run prettier
npm run lint:fix
Naming
- For
boolean
variables, use base word, do not include prefixes such asis
,has
, etc. unless meaning cannot be conveyed without it, such as to avoid confusion with another property.
Import statements
We have a couple of guidelines for import statements, which are enforced using the eslint rules
no-restricted-imports
,
import/order
,
and
import/no-restricted-paths
.
These rules aim to:
- Prevent relative imports across package boundaries.
- Restrict packages from importing application specific code.
- Enforce a convention for the order of import statements.
Imports within the same package
Use relative imports when importing within the same package. For example, MyNewService
and
LogService
are both in the @bitwarden/common
package.
import { LogService } from "../../abstractions/log.service";
export class MyNewService {}
Imports from different packages
For imports from different packages, use absolute imports. For example DifferentPackageService
is
not in @bitwarden/common
and needs to import LogService
from @bitwarden/common
.
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
export class DifferentPackageService {}
Avoid TypeScript enums
TypeScript enums are not fully type-safe and can cause surprises. Your code should use constant objects instead of introducing a new enum.
Our recommended approach (ADR-0025)
- Use the same name for your type- and value-declaration.
- Use
type
to derive type information from the const object. - Avoid asserting the type of an enum-like. Use explicit types instead.
- Create utilities to convert and identify enums modelled as primitives.
Numeric enum-likes
Given the following enum:
export enum CipherType {
Login: 1,
SecureNote: 2,
Card: 3,
Identity: 4,
SshKey: 5,
};
You can redefine it as an object like so:
// freeze to prevent member injection
export const CipherType = Object.freeze({
Login: 1,
SecureNote: 2,
Card: 3,
Identity: 4,
SshKey: 5,
} as const);
// derive the enum-like type from the raw data
export type CipherType = (typeof CipherType)[keyof typeof CipherType];
And use it like so:
// Can be imported together
import { CipherType } from "./cipher-type";
// Used as a type
function doSomething(type: CipherType) {}
// And used as a value (just like a regular `enum`)
doSomething(CipherType.Card);
// advanced use-case: discriminated union definition
type CipherContent =
| { type: typeof CipherType.Login, username: EncString, ... }
| { type: typeof CipherType.SecureNote, note: EncString, ... }
Unlike an enum, TypeScript lifts the type of the members of const CipherType
to number
. Code
like the following requires you explicitly type your variables:
// ✅ Do: strongly type enum-likes
let value: CipherType = CipherType.Login;
const array: CipherType[] = [CipherType.Login];
const subject = new Subject<CipherType>();
// ❌ Do not: use type inference
let value = CipherType.Login; // infers `1`
const array = [CipherType.Login]; // infers `number[]`
// ❌ Do not: use type assertions
let value = CipherType.Login as CipherType; // this operation is unsafe
String enum-likes
The above pattern also works with string-typed enum members:
// freeze to prevent member injection
export const CredentialType = Object.freeze({
Password: "password",
Username: "username",
Email: "email",
SshKey: "ssh-key",
} as const);
// derive the enum-like type from the raw data
export type CredentialType = (typeof CredentialType)[keyof typeof CredentialType];
Unlike string-typed enums, enum-likes do not reify a type for each member. This means that you can use their string value or their enum member interchangeably.
let value: CredentialType = CredentialType.Username;
// this is typesafe!
value = "email";
However, the string-typed values are not always identified as enum members. Thus, when the const object is in scope, prefer it to the literal value.
Utilities
The following utilities can be used to maintain type safety at runtime.
import { CipherType } from "./cipher-type";
const namesByCipherType = new Map<CipherType, keyof CipherType>(
Array.fromEntries(Object.entries(CipherType), ([k, v]) => [v, k]),
);
export function isCipherType(value: number): value is CipherType {
return namesByCipherType.has(value);
}
export function asCipherType(value: number): CipherType | undefined {
return isCipherType(value) ? value : undefined;
}
export function nameOfCipherType(value: CipherType): keyof CipherType | undefined {
return namesByCipherType.get(value);
}