Clean Code in TypeScript

Kyle Le
JavaScript in Plain English
4 min readNov 14, 2022

--

Introduction

Last week I wrote an article about code smells in TypeScript, the article received a lot of attention and praise. So I think many people want to know more about code smells in general and TypeScript to be more specific.

Remember, code smells or bad code doesn’t mean that the code is not usable. But as a programmer, I think we should produce reusable, readable, and refactorable software.

I will not repeat the mistakes in the last article, if you haven’t read it, please check it out

Don’t add unneeded context

If your class/type/object name tells you something, don’t repeat that in your variable name.

Should avoid:

type Car = {
carMake: string;
carModel: string;
carColor: string;
}

function print(car: Car): void {
console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}

Do this instead:

type Car = {
make: string;
model: string;
color: string;
}

function print(car: Car): void {
console.log(`${car.make} ${car.model} (${car.color})`);
}

Use enum

Enums can help you document the intention of the code. For example when we are concerned about values being different rather than the exact value of those.

Should avoid:

const GENRE = {
ROMANTIC: 'romantic',
DRAMA: 'drama',
COMEDY: 'comedy',
DOCUMENTARY: 'documentary',
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
// declaration of Projector
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
}
}
}

Do this instead:

enum GENRE {
ROMANTIC,
DRAMA,
COMEDY,
DOCUMENTARY,
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
// declaration of Projector
configureFilm(genre) {
switch (genre) {
case GENRE.ROMANTIC:
// some logic to be executed
}
}

Function names should say what they do

Should avoid:

function addToDate(date: Date, month: number): Date {
// ...
}

const date = new Date();

// It's hard to tell from the function name what is added
addToDate(date, 1);

Do this instead:

function addMonthToDate(date: Date, month: number): Date {
// ...
}

const date = new Date();
addMonthToDate(date, 1);

Prefer functional programming over imperative programming

Should avoid:

const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

let totalOutput = 0;

for (let i = 0; i < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}

Do this instead:

const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

const totalOutput = contributions
.reduce((totalLines, output) => totalLines + output.linesOfCode, 0);

Avoid negative conditionals

Should avoid:

function isEmailNotUsed(email: string): boolean {
// ...
}

if (isEmailNotUsed(email)) {
// ...
}

Do this instead:

function isEmailUsed(email: string): boolean {
// ...
}

if (!isEmailUsed(email)) {
// ...
}

Prefer immutability

Should avoid:

interface Config {
host: string;
port: string;
db: string;
}

Do this instead:

interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}

type vs. interface

Use type when you might need a union or intersection. Use an interface when you want extends or implements. There is no strict rule, however, use the one that works for you.

Should avoid:

interface EmailConfig {
// ...
}

interface DbConfig {
// ...
}

interface Config {
// ...
}

//...

type Shape = {
// ...
}

Do this instead:

type EmailConfig = {
// ...
}

type DbConfig = {
// ...
}

type Config = EmailConfig | DbConfig;

// ...

interface Shape {
// ...
}

class Circle implements Shape {
// ...
}

class Square implements Shape {
// ...
}

Single concept per test

Tests should also follow the Single Responsibility Principle. Make only one assert per unit test.

Should avoid:

import { assert } from 'chai';

describe('AwesomeDate', () => {
it('handles date boundaries', () => {
let date: AwesomeDate;

date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));

date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));

date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});

Do this instead:

import { assert } from 'chai';

describe('AwesomeDate', () => {
it('handles 30-day months', () => {
const date = new AwesomeDate('1/1/2015');
assert.equal('1/31/2015', date.addDays(30));
});

it('handles leap year', () => {
const date = new AwesomeDate('2/1/2016');
assert.equal('2/29/2016', date.addDays(28));
});

it('handles non-leap year', () => {
const date = new AwesomeDate('2/1/2015');
assert.equal('3/1/2015', date.addDays(28));
});
});

Conclusion

There are tons of things to say about clean code, one article isn't enough.
But if you want more detailed information, check out my reference.

Last Words

Although my content is free for everyone, but if you find this article helpful, you can buy me a coffee here

--

--

I’m a Software Engineer who loves to write. My content is based on what I've learned and experienced every day