Skip to content

TypeScript Refactor General Guidelines

0xdavinchee edited this page Jul 23, 2021 · 3 revisions

Introduction

Hey there Ohmie, I've written this guide to help bring you up to speed as quickly as possible when refactoring the existing codebase from .js/.jsx => .ts/.tsx as well as some general TypeScript principles/conventions when implementing new files/components. If you are interested in the reasoning for the introduction of TypeScript into the codebase, you can read this. This document is by no means complete/final.

Overarching Principles

Some principles to keep in mind when refactoring or developing in TS:

  1. Be more explicit about intent through the use of types.
  2. Put in the extra effort to define an unknown type if you are the first person dealing with it for both other developers and your future self.
  3. Please follow the guidelines as its purpose is to maintain a "common language" within the codebase between develoeprs so communication through code is seamless and productivity is optimized in the long run.

Guidelines

  1. Maintain logical consistency: refactoring the codebase to TS should only impact the syntax/semantics. The logic should remain the same after refactoring. This will result in the refactoring not leading to any bugs. If you do detect bugs or things not caught before, take note of it on a "TypeScript refactor caught bugs" list shared amongst all developers.
  2. Utilize strict mode: in tsconfig.json, we should set "strict": true, this will aid in the enforcement of the guidelines.
  3. Start by converting .js files then .jsx: this will yield the most benefits as this usually contains the bulk of the logic and will provide valuable typing information where these functions are used.
  4. When converting .jsx files, convert files from parent to child: Start refactoring the top-most components and moving down into child components otherwise you'll spend a lot of time jumping back and forth between parent and child files.
  5. Provide type information, early and often: For example, explicitly state the type when utilizing useState if the initial type is not a primitive (string, boolean, number, etc.) and implicitly state it when it is. For example:

An implicit declaration of a type:

const [loading, setLoading] = useState(false);
// the compiler/linter will know that loading is type boolean
// and will complain if you try to do something like:
// setLoading("");

An explicit declaration of a type:

// enum declaration
type Gender = "M" | "F";

// IUser interface declaration
interface IUser {
    readonly id: string;
    readonly name: string;
    readonly age: number;
    readonly gender: Gender;
}

const [user, setUser] = useState<IUser | null>(null);

// example of a TS function
const someFunc = async (id: string) => {
    const user: IUser = await getData(id); 
    setUser(user);
}
  1. Always define an interface for props: A corollary to the previous point, you should always define what props you expect a component to accept. This will ensure that any parent component utilizing this component in the future will know what props are required.

Example for what the props interface for Balance.jsx would look like:

interface IBalanceProps {
    readonly address: string;
    readonly balance: number;
    readonly dollarMultiplier: number;
    readonly price: number;
    readonly provider: StaticJsonRpcProvider;
    readonly size: number;
    readonly value: number;
}
  1. Naming conventions:
    • Prop Interfaces: I<COMPONENT_NAME>Props - IBalanceProps
    • Interfaces: I<INTERFACE_NAME> - IBondData
    • Use PascalCase for interface names and camelCase for the interface properties.
    • Define interface properties in alphabetical order.
    • Put function properties after all variable properties (alphabetical too):
interface IProps {
    readonly address: string;
    readonly name: string;
    readonly value: number;
    readonly getUserData: (id: string) => Promise<IUserData>;
    readonly setValue: (value: number) => void;
}
  1. You should almost NEVER use the any type unless you have good reason to do so. This destroys the whole purpose of using TypeScript and should be avoided 99.99% of the time, it is usually worth spending the extra time to uncover the type of something (as long as it's not defining types of an entire external library). Use unknown when in doubt.
  2. You should opt for using undefined or null over ? when defining properties in an interface for the most part unless you are dealing with overloads.
// Don't do this
interface Example {
    aFunc(x: string) => number;
    aFunc(x: string, y: number) => number;
    aFunc(x: string, y: number, z: boolean) => number;
}

// Do this
interface Example {
    aFunc(x: string, y?: string, z?: boolean) => number;
}

I doubt we will be doing this very much though.

  1. Do not use Number, String, Boolean, Symbol and Object, these are not types, they are JS primitives.
  2. Use readonly for interface properties if the intent is immutability of that property.
  3. Include interfaces at the top of the files after the imports.
  4. When refactoring, it's best to change the extension type and commit, then make changes on these files and commit again. This allows others to see the others (and yourself) to see the changes you made in the refactor.

Further reading

Clone this wiki locally