I am a Software Engineer with a passion for UI/UX design, backend development, and DevOps. Outside of work, I enjoy calisthenics, yoga, acrobatics, running, drawing, and CAM.

TypeScript is a superset of JavaScript that introduces optional static typing, which helps improve code quality and maintainability. While JavaScript is a widely-used, dynamic language, its lack of typing can make large applications difficult to manage over time. TypeScript adds type annotations, enabling developers to catch errors early and write more robust code. This feature brings JavaScript closer to statically-typed languages like Java and C#, offering better tooling and developer productivity while remaining fully compatible with existing JavaScript projects.
My recommended best practices for Typescript are both common and widely adopted. Moreover, these practices are versatile and can be applied to most object-oriented programming (OOP) languages.
Visit My General Coding Best Practices
Enable strict type checking to enforce non-null checks and prevent compilation when runtime errors are detected. This can be configured in the tsconfig.json file:
{
"compilerOptions": {
"strict": true
}
}
Avoid using any whenever possible. Overusing any makes the type system pointless, effectively treting the code like Javascript. Instead, use unknown when the type is uncertain, even though it requires additional code to validate and ensure the correct type.
Use interfaces (interface keyword) and types (type keyword) appropriately. Interfaces are ideal for defining simple, structured types and should generally be preferred. Types, on the other hand, are suited for more complex constructs such as unions, intersections, exclusion. In essence, types are used to perform operations on sets, similar to set theory in mathematics.
Leverage utility types to create complex constructs:
interface User {
name: string;
email: string;
age: number;
location: string;
}
Partial: all properties of an object become optional
type UserForm = Partial<User>;
Pick: pick one or more properties from an object
type AnonymousUser = Pick<User, 'age' | 'location'>;
Omit: omit one or more properties from an object
type IdentifiedUser = Omit<User, 'age' | 'location'>;
Ideally, use
PickorOmitwith fewer properties to enhance maintainability and ensure a more focused and manageable type structure.
const to ensure immutability and promote clearer, more predictable code. Always prefer const over let unless reassignment is necessary.readonly for properties that shoul not be modified, primarily in interfaces and classes, similar to the keyword final in Java.
interface User {
readonly dateOfBirth: Date;
}
function isMyClass(o: unknown): o is MyClass {
return o instanceof MyClass;
}
Use unknown for error handling, and perform instance type checks to handle errors appropriately:
try {
// Do something
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}
Ideally, use an error handler module combined with type guards to check the exception type, avoiding the need for multiple if statements in the catch block.
Create custom errors by extending the Error class to define specific error types tailored to your application’s needs:
class MyError extends Error {
constructor() {
super('Some message to pass to the Error constructor');
// Set the prototype explicitly
// to ensure instanceof works correctly
Object.setPrototypeOf(this, MyError.prototype);
}
}
Write pure components to enhance reusability. Pure components ensure consistent behaviour by rendering the same output for the same props or state.
Manage state using Higher Order Components (HOCs) that focus solely on handling the state of the rendered component. This approach keeps logic seperate while ensuring the rendered component remains pure and reusable. For example:
function MyStateManagementComponent(): JSX.Element {
const [isAlive, setIsAlive] = useState(false);
useEffect(( ) => {
if(!isAlive) {
setIsAlive(true);
}
}, [isAlive]);
return <MyPureComponent isAlive={isAlive}/>;
}
Manage state of the application with a context.
Define actions within the application using the domain language for naming actions.
// action.tsx
interface ResurrectionAction {
type: 'resurrect';
}
interface KillAction {
type: 'kill';
}
export type MyContextAction = ResurrectAction | KillAction;
export function resurrect(): ResurrectAction {
return {
type: 'resurrect';
};
}
export function kill(): KillAction {
return {
type: 'kill';
};
}
The context provides the state of the application and reacts to dispatched actions. This helps seperate concerns within the app.
// context.tsx
import {
Dispatch,
PropsWithChildren,
Reducer,
createContext,
useContext,
useReducer,
} from 'react';
import { MyContextAction } from './action';
interface MyContextState {
isAlive: boolean;
}
interface MyContextInterface extends MyContextState {
dispatch: Dispatch<MyContextAction>;
}
type MyContextReducer = Reducer<MyContextState, MyContextAction>;
const defaultMyContextState: MyContextState = {
isAlive: true
};
const MyContext = createContext<MyContextInterface>({
...defaultMyContextState,
dispatch() {
// Dummy impl
}
});
export function MyContextProvider(
props: PropsWithChildren<{}>
): JSX.Element {
const [state, dispatch] = useReducer<
MyContextReducer, MyContextState
>(
(prevState, action) => {
if(action.type === 'kill') {
return {
isAlive: false
};
} else if(action.type === 'resurrect') {
return {
isAlive: true
};
}
return prevState;
},
{
...defaultMyContextState
},
(state) => state
);
return <MyContext.Provider value={{...state, dispatch}}>
{props.children}
</MyContext.Provider>
}
export function useMyContext(): MyContextInterface {
return useContext(MyContext);
}
Use the context in components to dispatch actions based on user interactions. Below are examples of components that dispatch
resurrectandkillactions when triggered.
// resurrect.component.tsx
import { PureButton } from 'button.component';
import { useMyContext } from 'context';
import { resurrect } from 'action';
export function ResurrectComponent(): JSX.Element {
const { dispatch } = useMyContext();
return <PureButton
label={'Resurrect'}
onClick={() => dispatch(resurrect())}
/>;
}
// kill.component.tsx
import { PureButton } from 'button.component';
import { useMyContext } from 'context';
import { kill } from 'action';
export function KillComponent(): JSX.Element {
const { dispatch } = useMyContext();
return <PureButton
label={'Kill'}
onClick={() => dispatch(kill())}
/>;
}
Finally, the app provides the context and uses components named after the domain language for better readability and seperation of concerns.
// app.tsx
import { MyContextProvider } from 'context';
import { ResurrectComponent } from 'resurrect.component';
import { KillComponent } from 'kill.component';
export function App(): JSX.Element {
return <MyContextProvider>
<ResurrectComponent/>
<KillComponent/>
</MyContextProvider>
}
This page is still a work in progress