Key Sections:
- Case (
camelCasevs.PascalCasevs.ALL_CAPS) - Interface names
- Test filenames
- Type files
nullvs.undefinedconstvs.letvs.var- Formatting
- Quotes (single vs. double)
- Use semicolons
- Arrays (annotate as
Type[]) typevs.interface- One-line
ifstatements importsKeyboardEvents- Components
- Function return type
- Hooks
- Use
camelCasefor variable and function names
Reason: Conventional JavaScript
Bad
var FooVar;
function BarFunc() {}Good
var fooVar;
function barFunc() {}- Use
PascalCasefor component name, even if function component.
Reason: Follows React naming convention.
Bad
function component(): ReactElement {
return <subComponent />;
}Good
function Component(): ReactElement {
return <SubComponent />;
}- Use
PascalCasefor class names.
Reason: This is actually fairly conventional in standard JavaScript.
Bad
class foo {}Good
class Foo {}- Use
camelCaseof class members and methods
Reason: Naturally follows from variable and function naming convention.
Bad
class Foo {
Bar: number;
Baz() {}
}Good
class Foo {
bar: number;
baz() {}
}- Use
PascalCasefor name.
Reason: Similar to class
- Use
camelCasefor members.
Reason: Similar to class
- Use
PascalCasefor name.
Reason: Similar to class
- Use
camelCasefor members.
Reason: Similar to class
- Use
ALL_CAPSfor action types.
Reason: Follows Redux naming convention
- Use
PascalCasefor names
Reason: Convention followed by the TypeScript team. Namespaces are effectively just a class with static members. Class names are
PascalCase=> Namespace names arePascalCase
Bad
namespace foo {}Good
namespace Foo {}- Use
PascalCasefor enum names
Reason: Similar to Class. Is a Type.
Bad
enum color {}Good
enum Color {}- Use
PascalCasefor enum member
Reason: Convention followed by TypeScript team i.e. the language creators e.g
SyntaxKind.StringLiteral. Also helps with translation (code generation) of other languages into TypeScript.
Bad
enum Color {
red,
}Good
enum Color {
Red,
}Name files and folders with camelCase (e.g., utilities.ts, index.tsx, tractor.png, components/), unless it is
the name of the contained Component (e.g., DataEntryTable.tsx, ReviewEntries/).
Reason:
camelCaseis conventional across many JS teams.
- Don't prefix with
I
Reason: Unconventional.
lib.d.tsdefines important interfaces without anI(e.g. Window, Document etc).
Bad
interface IFoo {}Good
interface Foo {}-
The test file associated with
path/to/Component.tsxshould bepath/to/tests/Component.test.tsx. -
Auxiliary test files with data or mock objects should have filenames ending in
Mock.ts, e.g.,path/to/tests/DataMock.ts.
Reason:
*.test.*and*Mock.tsfiles are ignored by our CodeCov settings.
- Separate type files should contain
types,classes,interfaces,enums but not any classes or functions that need unit testings. Such files should have filenames ending inTypes.ts, e.g.,MergeDupsReduxTypes.ts.
Reason:
*Types.tsfiles are ignored by our CodeCov settings.
- Prefer not to use either for explicit unavailability
Reason: these values are commonly used to keep a consistent structure between values. In TypeScript you use types to denote the structure
Bad
let foo = { x: 123, y: undefined };Good
let foo: { x: number; y?: number } = { x: 123 };- Use
undefinedin general (do consider returning an object like{valid:boolean,value?:Foo}instead)
Bad
return null;Good
return undefined;- Use
nullwhere its a part of the API or conventional
Reason: It is conventional in Node.js e.g.
errorisnullfor NodeBack style callbacks.
Bad
cb(undefined);Good
cb(null);- Use truthy check for objects being
nullorundefined
Bad
if (error === null)Good
if (error)Remark: Use
===/!==to check fornull/undefinedon primitives that might be other falsy values (like'',0,false).
-
constallows the variable to be mutated, but doesn't allow it to be redeclared; preferconstany time your variable only needs to be declared once. -
letandvarboth allow redeclaration, butvaris a global variable andletis limited to the scope of its declaration; preferlettovar.
Bad
var shouldClapHands = false;
for (var i = 0; i < 3; i++) {
var you = getYouAtTime(i);
shouldClapHands ||= you.isHappy() && you.knowsIt();
}Good
let shouldClapHands = false;
for (let i = 0; i < 3; i++) {
const you = getYouAtTime(i);
shouldClapHands ||= you.isHappy() && you.knowsIt();
}Use Prettier to format TypeScript code as described in the README.
Reason: Reduce the cognitive overload on the team.
- Prefer double quotes (
"your text") to single quotes ('your text').
Reason: Follows Prettier naming convention.
- Use semicolons.
Reasons: Explicit semicolons helps language formatting tools give consistent results. Missing ASI (automatic semicolon insertion) can trip new devs e.g.
foo() \n (function(){})will be a single statement (not two). TC39 warning on this as well. Example teams: airbnb, idiomatic, google/angular, facebook/react, Microsoft/TypeScript.
- Annotate arrays as
foos:Foo[]instead offoos:Array<Foo>.
Reasons: Its easier to read. Its used by the TypeScript team. Makes easier to know something is an array as the mind is trained to detect
[].
- Use
typewhen you might need a union or intersection:
type Foo = number | { someProperty: number }
- Use
interfacewhen you wantextendsorimplementse.g
interface Foo {
foo: string;
}
interface FooBar extends Foo {
bar: string;
}
class X implements FooBar {
foo: string;
bar: string;
}
- Otherwise use whatever makes you happy that day.
- Add braces to one-line
ifstatements;
Good
if (isEmpty) {
callFun();
}
Bad
if (isEmpty)
callFun();
Reason: Avoiding braces can cause developers to miss bugs, such as Apple's infamous goto-fail bug
- Use absolute
importstatements everywhere for consistency.
Good
import { type Project } from "api/models";
import { getAllProjects } from "backend";Bad
import { type Project } from "../../../../api/models";
import { getAllProjects } from "../../../../backend";Reason: Provides consistency for imports across all files and shortens imports of commonly used top level modules. Developers don't have to count
../to know where a module is, they can simply start from the root ofsrc/.
- Generally import the specific things needed (e.g., not
Reactwhen{ type ReactElement }will do), and from a more specific target (e.g.,from "api/models"rather thanfrom "api"):
Good
import { type ReactElement } from "react";
import { type Project } from "api/models";
function Component(props: { project: Project }): ReactElement {}Bad
import React from "react";
import { type Project } from "api";
function Component(props: { project: Project }): React.ReactElement {}- Use
ts-key-enumwhen comparing toReact.KeyboardEvents.
Good
import { Key } from "ts-key-enum";
if (event.key === Key.Enter) {
}Bad
if (event.key === "Enter") {
}Reason: Avoid typos and increase the number of mistakes that can be caught at compile time.
- Prefer functional components to class components.
Good
function Component(props: ComponentProps): ReactElement {
return <SubComponent />;
}Bad
class Component extends React.Component<ComponentProps, ComponentState> {
render() {
return <SubComponent />;
}
}- Specify the return type of a named function, whether declared as a
functionorconst. - The return type of a functional component should be
ReactElement.
Good
import {ReactElement} from React;
function Component(props: { name: string }): ReactElement {
const sayHi = (name: string): string => {
return `Hi, ${name}!`;
}
return sayHi(props.name);
}Bad
function Component(props: { name: string }) {
const sayHi = (name: string) => {
return `Hi, ${name}!`;
};
return sayHi(props.name);
}Reason: Ensure all return paths are the expected type.
- Prefer
useAppDispatchtouseDispatchanduseAppSelectortouseSelector.
Good
import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
function Component(): ReactElement {
const dispatch = useAppDispatch();
const subState = useAppSelector((state: StoreState) => state.subState);
}Bad
import { useDispatch, useSelector } from "react-redux";
function Component(): ReactElement {
const dispatch = useDispatch();
const subState = useSelector((state) => state.subState);
}Reason: See
types/hooks.ts.