Skip to content

Instantly share code, notes, and snippets.

@fabecerram
Created November 14, 2023 19:16
Show Gist options
  • Save fabecerram/53f27afdc025602a86176ab7b44c9bac to your computer and use it in GitHub Desktop.
Save fabecerram/53f27afdc025602a86176ab7b44c9bac to your computer and use it in GitHub Desktop.
TypeScript Development Guidelines

TypeScript Development Guidelines

TypeScript is a widely used, open-source programming language that is perfect for modern development.

With its advanced type system, TypeScript allows developers to write code that is more robust, maintainable, and scalable.

But, to truly harness the power of TypeScript and build high-quality projects, it's essential to understand and follow best practices. In this guideline, we'll dive deep into the world of TypeScript and explore the best practices for mastering the language.

These best practices cover a wide range of topics and provide concrete examples of how to apply them in real-world projects. Whether you're just starting out or you're an experienced TypeScript developer, this information will provide valuable insights and tips to help you write clean, efficient code.

Some of these recommendations will even help us deal with some common security issues; when this is the case, it will be mentioned.


1 - Enable strict check on

use strict feature was added to JavaScript in ES5. It has the literal meaning which is "the code should be in strict mode".

This restricts you from doing unintentional or silly mistakes like using an undeclared variable, not using type annotations, or trying to use a future reserved keyword as a variable name, etc. 'use strict' helps you write good and secure code by showing bad coding habits as syntax errors.


2 - Naming convention & Code Styling

A style guide tells a developer how to work with a particular programming language. A coding style guide might inform programmers on such issues as acceptable naming conventions, how to store source files, and code format.

The main purpose of naming conventions is to keep your code and your work organized. Without any structure in your naming, analyzing large volumes of code or information could become chaotic.

In my GitHub you can find the guide TypeScript Programming Styles Guide


3 - Type inference & data type annotation

TypeScript is a typed language. However, it is not mandatory to specify the type of a variable. TypeScript infers types of variables when there is no explicit information available in the form of type annotations.

However, dynamic typing is a security problem, because it only checks types at runtime. Therefore, any security issues are caught while the application is running and not while it is under development.

Static typing checks types at compile-time, which prevents type issues before they become real problems or vulnerabilities.

When you are trying to remediate the source of an error or weakness, it takes longer to edit code in dynamic typing than in static typing. This is due to the expressive errors and compile-time type checks native to static typing. More expressive errors give more information about where the code's errors lie, such as the file and line number.

  • Variable types
name: string = "Hello";
value: number = 50;
isActive: boolean = false;
  • Parameter Type
private sumNumbers(firstNumber: number, secondNumber: number):number{
    return firstNumber + secondNumber;
}

Do not use ' any' keyword when you know what type of data your variable's gonna hold. It is recommended that you always define the data type whenever you declare a new variable.

Just like all types are assignable to any, all types are assignable to unknown. This makes unknown another top type of TypeScript's type system (the other one being any).

Fact: Types and strict typing are necessary for security purposes. Strict types prevent the reassignment or comparison of types without explicit declarations, there are multiple CWE's that can only be corrected and prevented using strict types.


4 - Use 'let' instead of 'var'

var is your good old friend JavaScript style of define variables, while let came into the picture in ES6 version. let and const were introduced to reduce some drawbacks TypeScript had with the use of var.

var is either a global scope or a local scope declaration. A var type variable becomes a globally scoped variable when it is defined outside a function/block.

In that case the variable is available to be used anywhere inside your script. var becomes locally scoped when it is defined inside a function. In those cases it is only accessible inside that function.

var name= "John Doe"; // global scope variable

function getAge() {
    var age= 30; // local scope variable
}

There are several drawbacks of var. It can be redeclared, can be called without declaring it, and TypeScript won't show any error but you will end up with surprising outputs.

var name = "John Doe";

function getName(){
    var name = "Anne"; // no error is showed
}

To avoid this, you should use let instead. let is a blocked scope variable declaration. And you cannot re-declare it. But you can declare the same variable name in different scopes, and each of them will be treated as separate different variables.

let name = "John";

if (true) {
    let name = "Anne";
    console.log(name); // "Anne"
}

console.log(name); // "John"

Fact: Avoid use 'var' is necessary for security purposes. Secure coding prevent redeclare variables and name collisions, it also provide better control over the scope of variables, there are multiple CWE's that can be corrected and prevented using this practice.


5 - Use 'const' for constants

const is also a blocked scope type. Also we cannot re-declare a const. However the purpose of const comes with the feature that it's value cannot be updated either (We could update with let). So always use const when you declare a constant.

const name = "John";
name = "Anne";// errorconst age = 30;

const age = 31; //error

Note: When declaring const objects, you cannot update it. But you can update it's properties.


6 - Use 'readonly' and 'readonlyarray'

TypeScript includes the readonly keyword that makes a property as read-only in the class, type or interface.

Mark properties that are never reassigned outside of the constructor with the readonly modifier. Read-only members can be accessed outside the class, but their value cannot be changed. Since read-only members cannot be changed outside the class, they either need to be initialized at declaration or initialized inside the class constructor.

class Employee {
    readonly empCode: number;
    empName: string;

    constructor(code: number, name: string) {
        this.empCode = code;
        this.empName = name;
    }
}

The ReadonlyArray type describes Arrays that can only be read from. Any variable with a reference to a ReadonlyArray can't add, remove, or replace any elements of the array.

function foo(arr: ReadonlyArray\<string\>) {
    arr.slice(); // okay
    arr.push("hello!"); // error!
}

While it's good practice to use ReadonlyArray over Array when no mutation is intended, it's often been a pain given that arrays have a nicer syntax. For example: number[] is a shorthand version of Array<number> , just as Date[] is a shorthand for Array<Date>. In that cases, use readonly modifier for array types.

function foo(arr: readonly string[]) {
    arr.slice(); // okay
    arr.push("hello!"); // error!
}

We can prefix any tuple type with the readonly keyword to make it a readonly tuple, much like we now can with array shorthand syntax. As you might expect, unlike ordinary tuples whose slots could be written to, readonly tuples only permit reading from those positions.

function foo(pair: readonly [string, string]) {
    console.log(pair[0]); // okay
    pair[1] = "hello!"; // error
}

7 -Use access modifiers

TypeScript includes access modifiers for properties of a class, classes are always public, but you can create public, protected or private properties.

  • private: allows access within the same class.
  • protected: allows access within the same class and subclasses.
  • public: allows access from any location.

Access modifiers change the visibility of the properties and methods of a class. They are useful for designing and organizing your code in OOP, as they help you to define the scope and the responsibility of each class, method, and field.

By using access modifiers, you can encapsulate the internal details of your classes, control inheritance and polymorphism, and avoid naming conflicts and accidental modifications.

This improves the readability, maintainability, security, and reduces the risk of errors and bugs in your code. Additionally, access modifiers ensure that you grant the minimum level of access necessary for the functionality of your code while avoiding exposing unnecessary or sensitive information to other classes or objects.

class Employee {
    protected name: string;
    private salary: number;

    constructor(name: string, salary: number) {
        this.name = name;
        this.salary = salary;
    }

    public getSalary(){
        return salary
    }
}

8 - Use enums

You can use enums to define a set of named constants and define standards that can be reused in your code base.

There is a way of generally grouping together different types of data utilized in code: discrete variables or continuous variables.

Discrete variables are data that have spaces between their representations, and have only a few representations. Example:

Days of the week

Mon
Tue
Wed
Thur
Fri
Sat
Sun

Discrete data is a good candidate to be placed inside an enum, and it can help code clarity and reuse.

Continuous data refers to data without gaps that fall into a continuous sequence, like numbers. Therefore, continuous data should not be used in an enum. Can you imagine an enum for age?

The recommendation is to export your enums once at the global level, and then let other classes import and use the enums.

Suppose you want to create a set of possible actions to capture the events in your codebase. TypeScript provides both numeric and string enums. The following example uses one enum.

enum EventType {
    Create,
    Delete,
    Update
}

class Event {

    constructor(event: EventType) {
        if (event === EventType.Create) {
            // Call for other function
            console.log(`Event Captured :${event}`);
        }
    }
}

let eventSource: EventType = EventType.Create;

const eventExample = new Event(eventSource)

9 - Avoid heterogenous enums

Technically speaking, enums can be mixed with string and numeric members, but it's not clear why you would ever want to do so.

Bad:

enum BooleanLikeHeterogeneousEnum {
    No = 0,
    Yes = "YES",
}

It's important to note that this is a discouraging practice, as in this case, using this method indicates that you probably need to:

  1. Rethink the relationship between these two variables
  2. Create two separate enums
  3. Make them both conform to a data type

10 - Use interfaces

Interfaces are useful in object-oriented programming because they specify the behavior that an object must implement. An interface is a contract between an object and its code. If an object implements an interface, it is guaranteed to support the behavior specified by that interface.

Interfaces make implementation changes less painful, because interfaces help you take advantage of polymorphism in OOP.

Modules are a common sight in large-scale applications, interfaces can serve as connecting points between modules too, and allow you to reveal only the behavior and services each module exposes to the outside world without mentioning the implementations. It gives access to a much less complex process while adding an extra security layer to the module.

interface Polygon {
    getArea(length: number, breadth: number):void;
}

Additionally, a proper use of interfaces makes your code easily testable, they allow you to quickly isolate the unit under testing, stopping other parts of the system from influencing the final results.


11 - Avoid empty interfaces

Empty interfaces (also known as "marker interfaces") are often used to mark a class for its intended purpose.

They exist for the sole purpose to make it possible so that the client code which is using the object that implements the marker interface can treat the object in a specific manner.

interface Bar {}

class Foo implements Bar {
    ...
}

//Client code...
foo = new Foo;

if (foo instanceof Bar) {
    ...
} else {
    ...
}

This allows some rudimentary type checking and gives the Foo class control over how it should be used by its clients.

But that is precisely the problem. This approach breaks encapsulation and the Single Responsibility Principle. The object itself now has indirect control over how it will be used externally. Moreover, it has knowledge of the system it's going to be used in.

By applying the marker interface, the class definition is suggesting it expects to be used somewhere that checks for the existence of the marker. It has implicit knowledge of the environment it's used in and is trying to define how it should be being used.

This goes directly against the idea of encapsulation because the class implementing the interface has knowledge of the implementation details of a part of the system that exists entirely outside its own scope.

By using marker interfaces in this way, the class now has two jobs, exposing its own interface and telling the outside world how it should be used. This is a discreet violation of the Single Responsibility Principle. In the same way the singleton pattern violates it by enforcing how the class can be used, marker interfaces hint at the same problem.

At a practical level this reduces portability and reusability. If the class is re-used in a different application, the interface needs to be copied across too, and it may not have any meaning in the new environment, making it entirely redundant.


12 - Use unknow type

The unknown type was introduced in TypeScript 3.0 as a way to represent a value of an unknown type. It is a top type, which means it can represent any possible value in TypeScript, similar to the any type.

However, unlike the any type, unknown enforces stricter type checking, helping programmers avoid common pitfalls and ensuring a higher level of type safety. Unknown is the type-safe counterpart of any.

The primary difference between the unknown and any types lies in the level of type checking. The any type is a complete opt-out from TypeScript's type checking, allowing any operation to be performed on it without any type errors.

In contrast, the unknown type forces developers to provide explicit type checks or assertions before performing operations on values of this type.

let data: any;

data = "hello";
console.log(data.toUpperCase()); // No error, even though data could be of any type

let otherData: unknown;

otherData = "world";
console.log(otherData.toUpperCase()); // Error: Object is of type 'unknown'

Typescript doesn't allow you to use a variable of unknown type unless you either cast the variable to a known type or narrow its type. Type narrowing is the process of moving a less precise type to a more precise type.

const x: unknown = 1;

if(typeof x === "number") {
    console.log(x \* x);
}

Ideally, we should avoid using unknown in the same way that we avoid using any, but if we find ourselves in a situation where we cannot avoid it, we should use unknown.

A specific case would be when we have multiple constructors, in the case of TypeScript their syntax is completely different from the standard of most OOP languages, and we will need to use unknown.


13 - Type guards

TypeScript is a very flexible language, even in terms of typing, to the point where we have top types that can contain values of any type, we have tuples, and even input parameters that can support multiple types. This flexibility does not come without challenges.

A type guard is a TypeScript technique used to get information about the type of a variable, usually within a conditional block. Type guards are regular functions that return a boolean, taking a type and telling TypeScript if it can be narrowed down to something more specific. Type guards have the unique property of assuring that the value tested is of a set type depending on the returned boolean.

TypeScript uses some built-in JavaScript operators like typeof , instanceof , and the in operator, which is used to determine if an object contains a property. Type guards enable you to instruct the TypeScript compiler to infer a specific type for a variable in a particular context, ensuring that the type of an argument is what you say it is.


  • The "instanceof" type guard

instanceof is a built-in type guard that can be used to check if a value is an instance of a given constructor function or class. With this type guard, we can test if an object or value is derived from a class, which is useful for determining the type of an instance type.

function sayHello(contact: Contact) {
    if (contact instanceof Person) {
        console.log("Hello " + contact.firstName);
    }
}

  • The "typeof" type guard

The typeof type guard is used to determine the type of a variable. The typeof type guard is said to be very limited and shallow. It can only determine the following types recognized by JavaScript:

boolean
string
bigint
symbol
undefined
function
number

For anything outside of this list, the typeof type guard simply returns object.

function StudentId(x: string | number) {
    if (typeof x == 'string') {
        console.log('Student');
    }

    if (typeof x === 'number') {
        console.log('Id');
    }
}

StudentId(`446`); //prints Student

StudentId(446); //prints Id

  • The "in" type guard

The in type guard checks if an object has a particular property, using that to differentiate between different types. It usually returns a boolean, which indicates if the property exists in that object.

type Fish = { swim: () => void };

type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        return animal.swim();
    }

    return animal.fly();
}

It should be noted that this kind of situation, where we have to do validations at the property level, is very unusual in OOP, and it is a clear indicator that we are doing something wrong, it is an alarm that tells us that we are going down the wrong path, and we are ignoring good design and development practices.


14 - Use never

never is a primitive type in TypeScript that represents a value that should never occur. This type is primarily used for type checking and ensuring the correctness of code during compilation. It is helpful in scenarios where a function should not return a value, a code branch should not be executed, or a value should never be assigned to a variable.

Using never allows us to ensure that we can control what happens when an unforeseen situation arises, involving a flow not previously identified or for which we were not prepared.

type Color = 'red' | 'green' | 'blue' | 'yellow';

function printColor(color: Color) {
    switch (color) {
        case 'red':
            console.log('The color is red');
            break;
        case 'green':
            console.log('The color is green');
            break;
        case 'blue':
            console.log('The color is blue');
            break;
        case 'yellow':
            console.log('The color is yellow');
            break;
        default:
            // This is a safety net to catch any missing cases
            const CHECK: never = color;
    }
}

It is quite common to use it in exception handling, from where we normally cannot return to the normal execution flow.

// function that always throws an error
function error(): never {
    throw new Error("Errors in the return type!");
}

// calling the error() function from the sample() function
function sample() {
    return error();
}

sample();

15 - Use TypeScript Utility Types

TypeScript utility types offer developers enhanced type manipulation capabilities by provide a set of tools that allow developers to transform one type into another. These tools can simplify complex type definitions and enhance type safety.

Since this is a fairly broad topic, it is difficult to go into much detail in this article, so I recommend reading the official documentation


16 - Use Linters

A linter is a tool that checks your code for errors, bugs, style issues, and best practices. It can help you write cleaner, more consistent, and more maintainable code. This is specially important when you're working in a team. Different developers have different coding habits.

There are many linters available for TypeScript and JS, such as ESLint, TSLint, or Prettier.

It is important to choose one that works well with your test frameworks, development frameworks and libraries, and your code style.


17 - Use a code formatter

In modern development, code formatters have become an essential tool for developers. Using a good code formatter makes your coding more efficient and cleaner by automate the process of code formatting.

The main purpose of code formatters is to standardize the formatting of code across a project or team, making it easier to read and understand code.

With code formatters, developers no longer need to spend time formatting code manually, which can save a lot of time and effort.

I'm use Prettier in VS code for TypeScript . But there are many code formatters out there and the choice depends on the programming language, frameworks or libraries, and the editor you use.


18 - Comments and documentation

Comments in code are used to include information on portions of source code in a project. Code comments are intended to make the related code easier to understand by providing additional context or explanation.

Code documentation is a collection of documents and code comments explaining how code works and how to use it. The form and size of documentation can vary.

Many developers don’t recognize the value of documenting code. They might argue that good code is self-explanatory. However, accurate documentation is essential for maintaining a codebase because it allows developers to quickly understand what the code does and how to work with it.

Good documentation is especially needed when many developers use code inside or outside your organization. Taking the time to document the code will make their work easier, and they’ll appreciate it. Good documentation should save more time than you spend writing it. In some cases, writing documentation can help identify overly complicated parts of the code and improve your architecture.

Speaking specifically about Typescript, the ideal is to follow the standards, it makes things easier for us and also helps the tools that specialize in compiling this documentation work better.

I personally recommend using TSDoc, is a proposal to standardize the doc comments used in TypeScript code, so that different tools can extract content without getting confused by each other's markup.

If our project includes api rest, it is recommended to use Swagger to keep our api properly documented.


Creator

Fabian A. Becerra M. https://github.com/fabecerram


Copyright and license

Code and documentation copyright 2019-2023 the authors. Code released under the MIT License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment