generated from scaffold-eth/scaffold-eth
-
Notifications
You must be signed in to change notification settings - Fork 696
TypeScript Refactor General Guidelines
0xdavinchee edited this page Jul 23, 2021
·
3 revisions
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.
Some principles to keep in mind when refactoring or developing in TS:
- Be more explicit about intent through the use of types.
- 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.
- 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.
- 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.
-
Utilize strict mode: in
tsconfig.json
, we should set"strict": true
, this will aid in the enforcement of the guidelines. - 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.
- 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.
-
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);
}
- 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;
}
-
Naming conventions:
- Prop Interfaces:
I<COMPONENT_NAME>Props
-IBalanceProps
- Interfaces:
I<INTERFACE_NAME>
-IBondData
- Use
PascalCase
for interface names andcamelCase
for the interface properties. - Define interface properties in alphabetical order.
- Put function properties after all variable properties (alphabetical too):
- Prop Interfaces:
interface IProps {
readonly address: string;
readonly name: string;
readonly value: number;
readonly getUserData: (id: string) => Promise<IUserData>;
readonly setValue: (value: number) => void;
}
- 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). Useunknown
when in doubt. - You should opt for using
undefined
ornull
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.
- Do not use
Number
,String
,Boolean
,Symbol
andObject
, these are not types, they are JS primitives. - Use
readonly
for interface properties if the intent is immutability of that property. - Include interfaces at the top of the files after the imports.
- 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.
- A quick primer on TS: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
-
https://github.com/OlympusDAO/olympus-frontend/issues/239 (initial proposal + reasons for TS + further in-depth examples on the
olympusdao-frontend
codebase) - https://discord.com/channels/838651642190495804/838654073464946758/866904265771057162 (our gameplan for refactoring)
- https://twitter.com/vadimdemedes/status/1400504057400233984 (inspiration on some of the guidelines)