Skip to main content

Typescript Style Guide

WWNorton TypeScript Style Guide

Typescript Lint Setup

Official Typescript Documentationโ€‹

Familiar With TypeScript already?

The starting point for learning TypeScript

TypeScript Best Practicesโ€Š-โ€ŠSemicolons and Spacing

Generalโ€‹

  • No Unused Expressions

We shouldn't have unused expressions in our code. If we have them, we should remove them.

// Bad ๐Ÿ‘Ž
a + b;

// Good ๐Ÿ‘
let add = a + b;
  • Filename

Name files with camelCase.

Reason: Conventional across many JS teams.

accordion.tsx
myControl.tsx
stringUtils.ts
map.ts

Variables and Functionโ€‹

  • Use camelCase for variable and function names

Reason: Conventional JavaScript

// Bad ๐Ÿ‘Ž
var FooVar;
function BarFunc() { }

// Good ๐Ÿ‘
var fooVar;
function barFunc() { }
  • No Unused Variables

@typescript-eslint/no-unused-vars

Likewise, if we have unused variables, then we should remove them.

// Bad ๐Ÿ‘Ž
var num1 = 10;
var num2 = 7;

var result = num1 + 7;

// Good ๐Ÿ‘
var num1 = 10;

var result = num1 + 7;

// Good ๐Ÿ‘
var num1 = 10;
var num2 = 7;

var result = num1 + num2;
  • Donโ€™t Use Variables Before theyโ€™re Defined

With variables that are declared withย var we can reference the variable before theyโ€™re defined, but the value will beย undefined. This is because the variable is hoisted. let andย const solve this problem since they arenโ€™t hoisted. Therefore, we should useย let orย const variables. This way, if we reference those variables, weโ€™ll get an error.

// Bad ๐Ÿ‘Ž
var hoisted;
console.log(hoisted); // will be undefined;

// Good ๐Ÿ‘
let notHoisted;
const alsoNotHoisted;

console.log(notHoisted); // Will throw an error (which is what we want in this case);
console.log(alsoNotHoisted); // Will also throw an error (which is what we want in this case);
  • Replace Magic Numbers with Named Constants

@typescript-eslint/prefer-as-const

If we have numbers that are used as constants repeatedly but arenโ€™t assigned to a constant, then we should assign it to one. This way, we can change it once and the value will be reflected everywhere. Also, named constants tell us the meaning of the number. Therefore, instead of writing:

// Bad ๐Ÿ‘Ž
enum Foo { bar = 1 }

// Good ๐Ÿ‘
const NUMBER_OF_BARS = 1;
enum Foo { bar = NUMBER_OF_BARS }
  • Donโ€™t Use async if await isnโ€™t Used Inside the Function

@typescript-eslint/require-await

We should useย asyncย functions only if we have toย awaitย something inside it.

For instance, if we have something like:

// Bad ๐Ÿ‘Ž
const foo = async () => "bar";

// Good ๐Ÿ‘
const foo = async () => {
const url = "some.domain.com";
let response = await fetch(url, {
method: 'GET'
});
}

then we should use a normal function.

  • Return Awaited Values Consistently

We should not haveย returnย andย awaitย on the same line since the promises may not have resolved yet.

Instead, put them on separate lines. The only exception to this is that we can putย returnย andย awaitย on the same line inside aย tryย block to catch errors from another promised-based function

// Bad ๐Ÿ‘Ž
async function foo() {
return await bar();
}

// Good ๐Ÿ‘
async function foo() {
const val = await bar();
return val;
}

// Good ๐Ÿ‘
async function foo() {
try {
return await bar();
} catch (error) {}
}

Arrayโ€‹

  • Annotate arrays as foos:Foo[] instead of foos: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 [].

// Bad ๐Ÿ‘Ž
foos:Array<Foo>;

// Good ๐Ÿ‘
foos:Foo[];

Stringsโ€‹

  • Prefer single quotes (') unless escaping.

Reason: More JavaScript teams do this (e.g. airbnb, standard, npm, node, google/angular, facebook/react). Its easier to type (no shift needed on most keyboards). Prettier team recommends single quotes as well

Double quotes are not without merit: Allows easier copy paste of objects into JSON. Allows people to use other languages to work without changing their quote character. Allows you to use apostrophes e.g. He's not going.. But I'd rather not deviate from where the JS Community is fairly decided.

// bad ๐Ÿ‘Ž
var foo = "bad";

// good ๐Ÿ‘
var foo = 'bar';
  • When you can't use double quotes, try using back ticks (`).

@typescript-eslint/restrict-template-expressions

Reason: These generally represent the intent of complex enough strings.

// good ๐Ÿ‘
var foo = `this foo has a dynamic ${bar}`;
  • Have Consistent Use of Backticks, Double Quotes or Single Quotes

We should use backticks or quotes in a consistent manner for declaring strings. Better yet, we should use backticks since theyโ€™ll create template strings, which are more flexible. They allow expressions to be embedded in it.

Classโ€‹

  • Use PascalCase for class names.

Reason: This is actually fairly conventional in standard JavaScript.

// Bad ๐Ÿ‘Ž
class fooBoo { }

// Good ๐Ÿ‘
class FooBoo { }
  • Use camelCase of class members and methods

Reason: Naturally follows from variable and function naming convention.

// Bad ๐Ÿ‘Ž
class Foo {
BarVar: number;
BazFunc() { }
}

// Good ๐Ÿ‘
class Foo {
barVar: number;
bazFunc() { }
}
  • No Useless Constructors

@typescript-eslint/no-useless-constructor

We shouldnโ€™t have useless constructors.

// Bad ๐Ÿ‘Ž
class A {
constructor () {
}
}

// Bad ๐Ÿ‘Ž
class B extends A {
constructor (value) {
super(value);
}
}

Theyโ€™re both redundant so they should be removed.

Interfaceโ€‹

  • Use PascalCase for name.

Reason: Similar to class

// Bad ๐Ÿ‘Ž
interface themeProps {
color: string;
}

// Good ๐Ÿ‘
interface ThemeProps {
color: string;
}
  • Use camelCase for members.

Reason: Similar to class

// Bad ๐Ÿ‘Ž
interface ColorProps {
FirstColor: string;
second-color: string;
}

// Good ๐Ÿ‘
interface ColorProps {
firstColor: string;
secondColor: string;
}
  • Don't prefix with I

Reason: Unconventional. lib.d.ts defines important interfaces without an I (e.g. Window, Document etc).

// Bad ๐Ÿ‘Ž
interface IFoo {
}

// Good ๐Ÿ‘
interface Foo {
}

Typeโ€‹

  • Use PascalCase for name.

Reason: Similar to class

// Bad ๐Ÿ‘Ž
type infoBaseProps = 'htmlFor' | 'className' | 'children' | 'id';

// Good ๐Ÿ‘
type InfoBaseProps = 'htmlFor' | 'className' | 'children' | 'id';
  • Use camelCase for members.

Reason: Similar to class

// Bad ๐Ÿ‘Ž
type Animal = {
animalrace: string
}

// Good ๐Ÿ‘
type Animal = {
animalRace: string
}

Type vs. Interfaceโ€‹

  • Use type when you might need a union or intersection:
// Good ๐Ÿ‘
type Foo = number | { someProperty: number }
  • Use interface when you want extends or implements e.g
// Good ๐Ÿ‘
interface Foo {
foo: string;
}

interface FooBar extends Foo {
bar: string;
}

class X implements FooBar {
foo: string;
bar: string;
}

Namespaceโ€‹

  • Use PascalCase for names

Reason: Convention followed by the TypeScript team. Namespaces are effectively just a class with static members. Class names are PascalCase => Namespace names are PascalCase

// Bad ๐Ÿ‘Ž
namespace fooBaz {
}

// Good ๐Ÿ‘
namespace FooBaz {
}

Enum

  • Use PascalCase for enum names

Reason: Similar to Class. Is a Type.

// Bad ๐Ÿ‘Ž
enum colorOne {
}

// Good ๐Ÿ‘
enum ColorOne {
}
  • Use PascalCase for 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 {
redHue
}

// Good ๐Ÿ‘
enum Color {
RedHue
}

Null vs. Undefinedโ€‹

  • 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 undefined in general (do consider returning an object like {valid:boolean,value?:Foo} instead)
// Bad ๐Ÿ‘Ž
return null;

// Good ๐Ÿ‘
return undefined;

  • Use null where its a part of the API or conventional

Reason: It is conventional in Node.js e.g. error is null for NodeBack style callbacks.

// Bad ๐Ÿ‘Ž
cb(undefined)

// Good ๐Ÿ‘
cb(null)

  • Use truthy check for objects being null or undefined
// Bad ๐Ÿ‘Ž
if (error === null)

// Good ๐Ÿ‘
if (error)

  • Use == null / != null (not === / !==) to check for null / undefined on primitives as it works for both null/undefined but not other falsy values (like '',0,false) e.g.
// Bad ๐Ÿ‘Ž
if (error !== null) // does not rule out undefined

// Good ๐Ÿ‘
if (error != null) // rules out both null and undefined

Tabs vs. Spacesโ€‹

  • Use tabs over spaces.

Reason: Use tabs for indenting your code. With tabs users can choose their desired width. This has positive implications for accessibility and screenreaders, putting people in control of how they want to view the code.

// bad ๐Ÿ‘Ž
function foo() {
โˆ™โˆ™let name;
}

// bad ๐Ÿ‘Ž
function bar() {
โˆ™let name;
}

// good ๐Ÿ‘
function baz() {
โˆ™ let name;
}

Semicolonsโ€‹

  • Use semicolons.

Reasons: The usage of semicolons is a core value of our community. Consider the points of the opposition, but be a traditionalist when it comes to abusing error correction mechanisms for cheap syntactic pleasures. Explicit semicolons helps language formatting tools give consistent results. Missing ASI (automatic semicolon insertion) can trip new devs.

// Bad ๐Ÿ‘Ž
let x = 5

// Good ๐Ÿ‘
let x = 5;

We should put in semicolons in our code instead of letting the JavaScript interpreter add them for us in unexpected places.

  • Remove Useless Semicolons

@typescript-eslint/no-extra-semi

We should remove duplicate semicolons in our code. We only need one semicolon at the end of a statement.

// Bad ๐Ÿ‘Ž
let x = 5;;

// Good ๐Ÿ‘
let x = 5;
  • Require Semicolons Instead of Automatic Semicolon Insertion

Instead of letting the Javascript interpreter put in semicolons for us, we should put them in ourselves.

// Bad ๐Ÿ‘Ž
function returnObject() {
return //<-----semi colon will be inserted causing an error.
{
key: value
};
}

// Good ๐Ÿ‘
function returnObject() {
return {
key: value
};
}

Import / Exportโ€‹

  • Avoid export default

Avoid export default in TypeScript because of the maintainability and DX issues outlined in Typescript Deep Dive - Avoid Export Default.

Example:

// foo.ts
class Foo {
}
export default Foo;
// bar.ts
import Foo from "./foo";
  • If you refactor Foo in foo.ts it will not rename it in bar.ts.
  • If you end up needing to export more stuff from foo.ts (which is what many of your files will have) then you have to juggle the import syntax
// Bad ๐Ÿ‘Ž
// foo.ts
export default class Foo {
return
{
key: value
};
}

// bar.ts
import Foo from './foo';


// Good ๐Ÿ‘
// foo.ts
export class Foo {
return {
key: value
};
}

// bar.ts
import { Foo } from "./foo";