Clean Code in TypeScript
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
More content at PlainEnglish.io. Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord. Interested in Growth Hacking? Check out Circuit.