React Typescript quick reference

Define types in class-components,

Use angle brackets.

interface CounterProps { 
    message: string;
};

interface CounterState {
    count: number; 
};

class Counter extends React.Component<CounterProps, CounterState> { 
    state: CounterState = {
        count: 0 
    };
    render() { 
        return (
            <div>
                {this.props.message} {this.state.count}
            </div> 
        );
    }
}

const containerStyles: React.CSSProperties = {
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
}

Defining Functional components

export const Example = () => {
    return <div>Functional component text</div>
}

Here we return a string wrapped into a <div/> element, Typescript will automatically conclude(infer) that the return type of our function is JSX.Element.

But if you wish you can use React.FC or React.FunctionalComponent

export const Example: React.FC = () => { 
    return <div>Functional component text</div>
}

Using props with functional components. In Typescript, you need to provide a type or an interface to define the form of your props object. In a lot of cases, types and interfaces can be used interchangeably because a lot of their features overlap.

interface ExampleProps {
    title: string
}

export const Example = ({ string }: ExampleProps) => {
    return <div>Functional component text</div>
}

Above we define a string type for a title which will be required by default but to make it optional we add a question mark.

// meaning the title must be either a string or undefined(string | undefined)
interface ExampleProps {
    title?: string
}

With Children props

You could simply add it to our interface like below.

interface ExampleProps {
    title: string,
    children: React.ReactNode
}

export const Example = ({
    title, 
    children
} :ExampleProps) => {
    return <div>Functional component text</div>
}

Or in a more cleaner way (I think).

interface ExampleProps {
    title: string
}

export const Example = ({
    title, 
    children
} :React.PropsWithChildren<ExampleProps>) => {
    return <div>Functional component text</div>
}

The type definition of React.PropsWithChildren looks something like

type React.PropsWithChildren<P> = P & {
    children?: React.ReactNode;
}

Don't mind the P, it's a generic placeholder representing ExampleProps in our Example component above.

With Styled Components

export const Button = styled.button<ButtonProps>`
    background-color: #ffffff3d;
    border-radius: 3px;
    border: none;
    color: ${props => (props.dark ? "#000" : "#fff")};
    cursor: pointer;
    padding: 10px 12px;
    &:hover {
        background-color: #ffffff52;
    }
`

With useRef

useRef provides a way to access the actual DOM nodes of rendered React elements. Typescript can't automatically infer the element type so we have to specify it. For Example, accessing a rendered input element.

const ref = useRef<HTMLInputElement>(null)

Visit @types/react/global.d.ts to know the element types exposed globally.

With useReducer

A basic reducer.

const counterReducer = (state: State, action: Action) => {
    switch (action.type) {
        case "increment":
            return { count: state.count + 1 }
        case "decrement":
            return { count: state.count - 1 }
        default:
            throw new Error()
    }
}

We can easily define the State interface.

interface State {
    count: number
}

To cater for multiple possibilities of the reducer type field we use Unions.

Note: To deal with Unions, intersections and primitives we use type as opposed to interface which works with objects.

export type Action =
    | {
        type: "increment"
    }
    | {
        type: "decrement"
    }

We are passing two interfaces separated by vertical lines, meaning Action can resolve to one of them. But you will notice that we are not using standard types like string, number... but string values "increment" and "decrement", this technique is called discriminated union. A good example to understand this.

export type Action = 
    | {
        type: "ADD_ITEM"
        payload: { text:  string, itemId: string }
    }
    | {
        type: "REMOVE_ITEM"
        payload: string
    }

The fact that both interfaces are different, having a type of "ADD_ITEM" or "REMOVE_ITEM " can help Typescript to determine what to expect from the payload, for example type "ADD_ITEM" will expect an object whereas "REMOVE_ITEM" will expect a string. At this point there is no need to declare constants for action types because Typescript will return an error if the type is not "ADD_ITEM" or "REMOVE_ITEM"

Note: It's always good practice to wrap your switch cases in curly brackets, the values can overlap between these cases

With React Context

Assume we what to avail a user object across the application through the context

interface User {
    fullname: string
    authToken: string
}

const user: User = {
    fullname: "Roghashin Timbiti",
    authToken: "vvnv_bkkldw900"
}

const AppContext = createContext()

React will demand us to provide the default value for our context. This value will only be used if we don’t wrap our application into a AppStateProvider, which we are doing.
To omit the issue, we pass an empty object and cast it to our User interface. It should look something like.

const AppContext = createContext<User>({} as User)

// and now use AppContext
export const AppContextProvider = ({ children }: React.PropsWithChildren<{}>) => {
    return (
        <AppContext.Provider value={user}>
            {children}
        </AppContext.Provider>
    )
}

Our component only accepts the children prop, since we have no others will just pass an empty object.

With Reusable functions

Declaring types for a function that expects to be called with parameters which are not specified can be daunting (well, except if you run back to type any 😅 ).
To accommodate multiple possible types we look to Generics.

Dumbest example
A function that returns the length of a list/array

const listCount = <T>(list: T[]): number => {
    return list.length
}

The function listCount takes an array as a parameter but the big question, Should it be an array of string, numbers, objects, booleans?

The fact that the items in the list can be strings or number or booleans or e.t.c. doesn't matter at this point because generic act as a placeholder for whatever type that is passed and also makes sure that its usage is persisted even within the function.
Our intention is to make sure that our function is reusable by all kinds of array items.

Generic constraints

Say we just want to restrict/constraint these generics to certain types. For example, a function that returns the index of an object in an array by it's id

interface Item {
    id: string
}

export const findItemIndexById = <T extends Item>(items: T[], id: string) => {
    return items.findIndex((item: T) => item.id === id)
}

The generic constraint makes sure that the first parameter of the function is any array of objects where each object has at least a key of id