Create project
npm create vite@latest project-name
npm i ts-node-dev -D
//package.json
{
"scripts": {
"dev": "ts-node-dev --respawn src/index.ts"
}
}
Add linter
# initialize the linter with
npx eslint --init
# or
npm init @eslint/config@latest
// eslint.config.js
export default [
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
languageOptions: {
globals: globals.browser,
parser: tseslint.parser,
parserOptions: {
ecmaVersion: 2021,
sourceType: "module",
ecmaFeatures: {
jsx: true
}
}
},
plugins: ["react"],
settings: {
react: {
version: "detect"
}
},
rules: {
"react/react-in-jsx-scope": "off",
"react/jsx-uses-react": "off"
}
},
...tseslint.configs.recommended,
pluginReact.configs.recommended
];
React.FC
React.FC(short forReact.FunctionComponent) is a type that defines a function which is expected to be a valid React component.
const App: React.FC = () => {
// ✔️
return <h1>Todo List App</h1>;
};
- TypeScript will infer it correctly if it is not specified.
const App = () => {
// ✔️
return <h1>Todo List App</h1>;
};
Adding Types to Props
To enable type checking for the props, we first need to declare types for them.
- You can inline the props type declaration.
const App = ({ message }: { message: string }) => {
return <div>{message}</div>;
};
- Or first define the expected props types.
type AppProps = {
message: string;
};
// If exporting, use `interface` so that consumers can extend it.
interface AppProps {
message: string;
}
- And then add the props type as an argument.
const App = ({ message }: AppProps): React.JSX.Element => {
return <div>{message}</div>;
};
JSX.Elementis the result of rendering aReact.FC, not a component in itself.
- The return type is inferred, so this syntax is preferred ✅
const App = ({ message }: AppProps) => {
return <div>{message}</div>;
};
- We can also add type checking like this. Since React 18+, this does not add a
childrenproperty.
const App: React.FC<AppProps> = ({ message }) => {
return <div>{message}</div>;
};
useState
- Type checking gives us real-time error messages in the development environment when, for example, an expected number is actually a string.
function App() {
const [num, setNum] = useState(5); // number inferred
const changeNumber = () => {
setNum("2"); // ❌
};
return (
<div className="App">
{num}
<button onClick={changeNumber}>Change number</button>
</div>
);
}
export default App;
function App() {
const [num, setNum] = useState<number | string>(5);
const changeNumber = () => {
setNum("2"); // ✔️
};
return (
<div className="App">
{num}
<button onClick={changeNumber}>Change number</button>
</div>
);
}
export default App;
List of Components
- In TypeScript, we need to specify the shape of the arrays.
function App() {
const [subs, setSubs] = useState([]); // ❌
return (
<div className="App">
<h1>subs</h1>
<ul>
{subs.map((sub) => {
return (
<li key={sub.nick}>
<img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
<h4>
{sub.nick} <small>{sub.subMonths}</small>
</h4>
<p>{sub.description?.substring(0, 100)}</p>
</li>
);
})}
</ul>
</div>
);
}
- For this, we first need to create the type of the items that the array will have in it.
interface Sub {
nick: string;
avatar: string;
subMonths: number;
description?: string;
}
function App() {
const [subs, setSubs] = useState<Array<Sub>>([]); // ✔️
return (
<div className="App">
<h1>subs</h1>
<ul>
{subs.map((sub) => {
return (
<li key={sub.nick}>
<img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
<h4>
{sub.nick} <small>{sub.subMonths}</small>
</h4>
<p>{sub.description?.substring(0, 100)}</p>
</li>
);
})}
</ul>
</div>
);
}
Abstracting
- We can abstract the App state like this.
interface Sub {
nick: string;
avatar: string;
subMonths: number;
description?: string;
}
interface AppState {
subs: Array<Sub>;
}
function App() {
const [subs, setSubs] = useState<AppState["subs"]>([]); // ✔️
return (
<div className="App">
<h1>subs</h1>
<ul>
{subs.map((sub) => {
return (
<li key={sub.nick}>
<img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
<h4>
{sub.nick} <small>{sub.subMonths}</small>
</h4>
<p>{sub.description?.substring(0, 100)}</p>
</li>
);
})}
</ul>
</div>
);
}
- We could also abstract the list of items into a new component, pass it the props, and then import it into the App component.
// List.tsx
interface Sub {
nick: string;
avatar: string;
subMonths: number;
description?: string;
}
interface Props {
subs: Array<Sub>;
}
export default function List({ subs }: Props) {
// ✔️
return (
<ul>
{subs.map((sub) => {
return (
<li key={sub.nick}>
<img src={sub.avatar} alt={`Avatar for ${sub.nick}`} />
<h4>
{sub.nick} <small>{sub.subMonths}</small>
</h4>
<p>{sub.description?.substring(0, 100)}</p>
</li>
);
})}
</ul>
);
}
// App.tsx
import List from "./List";
interface Sub {
nick: string;
avatar: string;
subMonths: number;
description?: string;
}
interface AppState {
subs: Array<Sub>;
}
function App() {
const [subs, setSubs] = useState<AppState["subs"]>([]); // ✔️
return (
<div className="App">
<h1>subs</h1>
<List subs={subs} />
</div>
);
}
- The interfaces should actually be in a
types.d.tsfile so we can avoid code repetition and import them in bothApp.tsxandList.tsx.
// types.d.ts
export interface Sub {
nick: string;
subMonths: number;
avatar: string;
description: string;
}
Forms
- To abstract the Form state, it is recommended to separate the business logic types from the component types. Instead of reusing the
Subinterface, we will create one for the form.
import { Sub } from "../types";
interface FormState {
inputValues: Sub;
}
const Form = () => {
const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
nick: "",
subMonths: 0,
avatar: "",
description: ""
});
// form state setup
};
React.ChangeEvent<HTMLInputElement>represents an event that occurs when the value of an HTML<input>element changes in a React application.
// form change handler
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
setInputValues({
...inputValues,
[evt.target.name]: evt.target.value
});
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
onChange={handleChange}
value={inputValues.nick}
type="text"
name="nick"
placeholder="nick"
/>
<button>Save new sub!</button>
</form>
</div>
);
- Adding props to the Form component and passing them from the App component.
// App.tsx
function App() {
const [subs, setSubs] = useState<AppState["subs"]>([]);
const [newSubsNumber, setNewSubsNumber] =
useState<AppState["newSubsNumber"]>(0);
useEffect(() => {
setSubs(INITIAL_STATE);
}, []);
return (
<div className="App">
<h1>midu subs</h1>
<List subs={subs} />
<Form onNewSub={setSubs} />
</div>
);
}
export default App;
// Form.tsx
interface FormProps {
onNewSub: React.Dispatch<React.SetStateAction<Sub[]>>;
}
const Form = ({ onNewSub }: FormProps) => {
const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
nick: "",
subMonths: 0,
avatar: "",
description: ""
});
};
const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onNewSub((subs) => [...subs, inputValues]);
};
- It is better to make a new function to handle the state change instead of passing the setter directly through props.
// App.tsx
function App() {
const [subs, setSubs] = useState<AppState["subs"]>([]);
const [newSubsNumber, setNewSubsNumber] =
useState<AppState["newSubsNumber"]>(0);
useEffect(() => {
setSubs(INITIAL_STATE);
}, []);
const handleNewSub = (newSub: Sub): void => {
setSubs((subs) => [...subs, newSub]);
};
return (
<div className="App">
<h1>midu subs</h1>
<List subs={subs} />
<Form onNewSub={handleNewSub} />
</div>
);
}
export default App;
interface FormState {
inputValues: Sub;
}
interface FormProps {
onNewSub: (newSub: Sub) => void;
}
const Form = ({ onNewSub }: FormProps) => {
const [inputValues, setInputValues] = useState<FormState["inputValues"]>({
nick: "",
subMonths: 0,
avatar: "",
description: ""
});
const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onNewSub(inputValues);
};
};
useRef
- To utilize the
useRefhook, we need to indicate through generics which data type it will be used with.
import { useRef } from "react";
const divRef = useRef<HTMLDivElement>(null);
<div className="App" ref={divRef}></div>;
useReducer
type FormReducerAction =
| {
type: "change_value";
payload: {
inputName: string;
inputValue: string;
};
}
| {
type: "clear";
};
const formReducer = (
state: FormState["inputValues"],
action: FormReducerAction
) => {
switch (action.type) {
case "change_value": {
const { inputName, inputValue } = action.payload;
return {
...state,
[inputName]: inputValue
};
}
case "clear":
return INITIAL_STATE;
default:
return state;
}
};
const Form = ({ onNewSub }: FormProps) => {
const [inputValues, dispatch] = useReducer(formReducer, INITIAL_STATE);
const handleSubmit = (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
onNewSub(inputValues);
handleClear();
};
const handleChange = (
evt: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = evt.target;
dispatch({
type: "change_value",
payload: {
inputName: name,
inputValue: value
}
});
};
const handleClear = () => {
dispatch({ type: "clear" });
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
onChange={handleChange}
value={inputValues.name}
type="text"
name="name"
/>
<button onClick={handleSubmit} type="submit">
Clear
</button>
</form>
</div>
);
};
Fetching
- To fetch an API, we need to specify the type of the data that its promise returns.
useEffect(() => {
const fetchSubs = (): Promise<SubsResponseFromApi> => {
return fetch("http://localhost:3001/subs").then((res) => res.json());
};
fetchSubs().then(setSubs);
}, []);
- We can also use
axiosand specify the return data type in itsgetmethod.
useEffect(() => {
const fetchSubs = () => {
return axios
.get<SubsResponseFromApi>("http://localhost:3001/subs")
.then((response) => response.data);
};
fetchSubs().then(setSubs);
}, []);