I went bravely from using js to jsx files. Then I overcame the gag reflex to write html in javascript. It was time to cover the final threshold with TypeScript and the tsx filetype. In this article, I cover briefly, how to use TypeScript with React.
Defining types within functions
If you’re familiar with TypeScript, then this is how you’re used to declaring your types in functions. This will be important when you also apply this to components:
function convertCurrency(amount: number, currency:string) {}
If the function is returning something, you’ll also need to declare the return type (a common setup for React components):
function myAge(age:number): number {
return age
}
Using Props the TypeScript way
Props is stored as an object so we also need to define it as such:
function Button(props: { headingText: string }) {
const { headingText } = props
}
// or
function Button({ headingText }:{ headingText: string }) {}
There are two main methods of declaring props with TypeScript, using interface
and type
. But they handle things slightly differently. A type
can store anything, but an interface
is always an object. Here’s an example using type
:
type Color = "red" | "green"
This is known as a restricted type, and will only accept these values (called a union). the string type is inferred so we don’t need to declare it.
Here’s an example using interface
. Note it’s being declared as an object ( so the above union declared as an interface
would not be possible – it would have to be placed within an object):
interface ButtonProps {
headingText?: string
myColor: Color
myOtherColor?: Color
someArray: boolean[] // array of booleans, unlimited
}
function Button({headingText, myColor, myOtherColor, someArray}: ButtonProps) {}
The use of question mark here is known as optional chaining, and will make the prop an optional value.
The default inferred type of a jsx element in React is React.JSX.Element
. You can also import types from other files, like so:
import {type Color} from 'your/types/file'
Using Props for any inline style
You may often find yourself applying inline styles to your components. Rather than defining a bunch of props, you can just use everything that’s available:
type ButtonProps = { style: React.CSSProperties }
function Button({style}: ButtonProps) {}
Then you can add any inline style you want to your component, as you normally would with vanilla javascript (backgroundColor, borderRadius etc etc).
Using props for any available element attribute
Similar to using styles, you don’t need to declare a prop for every element attribute. Just use the following (where button is the element type):
function Button({...props}: React.ComponentProps<"button">){
return <button {...props}>click</button>
}
This will allow props for any of the available attributes for button. You can also use React.ComponentPropsWithRef
and React.ComponentPropsWithoutRef
, depending on how you want to use refs reclared on your component.
Note that you can also use things like HTMLElement
to declare type, as well as other methods. There are pros and cons, but using the above seems to be the best solution. You might also see other methods used in the wild like React.FC<props>
. Whilst this is not necessarily incorrect, it’s not used as much these days.
Read only values
Not to much a React thing, but really a TypeScript thing, it’s useful to know you can set variables to read only:
const textArray = ['one', 'two', 'three'] as const
Handling Events
How you handle events will depend on the context. You don’t need to declare type when events are handled inline (the event is inferred):
function Button(){
return <button onClick={event => console.log('clicked')}></button>
}
But you do if you use a seperate event handler. It’s sometimes tricky to work out what’s required as the type, but intellisense in vs code will give you hints if you’re unsure:
const handleClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => console.log('clicked')
return <button onClick={handleClick}></button>
Declaring component children
Rather than prop drilling, using nested children is a great alternative to pass values between your components. Using React.ReactNode
will use any type of child (including plain old text strings). If you replace it with React.JSX.Element
, then only JSX elements will be allowed :
type ButtonProps = { children: React.ReactNode }
function Button({children}: ButtonProps) {
return <button>{children}</button>
}
Using state in components
Passing through states in TypeScript can be a bit more tricky. But again, intellisense should be able to help point the way:
type ButtonProps = {
setCount: React.Dispatch<React.SetStateAction<number>>
}
function Button({setCount}: ButtonProps) {}
The useState
itself can be declared like so, though this is rarely needed as the initial set value will infer the type. In this example, 0 is a number, so also declaring it as a number is redundant:
const [count, setCount] = useState<number>(0)
Here’s an example where declaring the type is useful. There are two possible values for useState here (User or null) so the type reflects this:
type User = {name: string, age: number}
const [user, setUser] = useState<User | null>(null)
const name = user?.name
Intersect and omit to add to or change existing types
In some cases you might want to add more props after you’ve declared them, after the fact. The ampersand used here will intersect, adding the otherValue
prop to the existing type. When applying this with interface
, you would use extend
:
type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
otherValue: string
}
// or
type BaseButtonProps = {
color: "red", "green", "blue"
}
type SuperButtonProps = BaseButtonProps & {
colorType: "primary", "secondary"
}
You can also refer to an existing type, using some removed values using omit
:
type User = {
sessionID: string,
name: string
}
type Guest = Omit<User, "name">
Setting generics with type
Probably one that’s not use too often, but is useful to know. Rather than leaning on the any
type (which is not great for error handling), you can keep your input type the same as your output type like so:
const convertToArray = <T,>(value:T): T[] => {
return [value]
}
The value T in this case could be anything (a number , an array and so on). Writing <T,>
sets up this relationship, and using the comma prevents JSX from confusing it as an element. To avoid this, you can also just write the function as follows:
function convertToArray<T>(value: T): T[] {
return [value]
}
You can also create a relationship between two props, to ensure that they are of the same type:
type ButtonProps<T> = {
countValue: T
countHistory: T[]
}
function Button<T>({countValue, countHistory}: ButtonProps<T>){}
Accept any children AND any native element Prop
The holy grail of React and TypeScript:
type ButtonProps = React.ComponentPropsWithoutRef<"button> & {
children: React.ReactNode
};
const Button = ({ children, ...props }: ButtonProps) => {
return (
<button {...props}>
{children}
</button>
);
};
export default Button
Hint: save this as a snippet when you use typescript with react in your favourite IDE, and all your React dreams will become a reality.