├── .gitignore ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── not-typesafe.ts ├── streamlining-with-zod.ts └── using-unknown.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Safe and reliable `fetch` calls in TypeScript. 2 | 3 | - [src/not-typesafe.ts](./src/not-typesafe.ts) shows the most common mistake people make when using `fetch` in TypeScript. 4 | - [src/using-unknown.ts](./src/using-unknown.ts) shows how to use `unknown` to make the code safer. 5 | - [src/streamlining-with-zod.ts](./src/streamlining-with-zod.ts) shows how to use [Zod](https://zod.dev) to easily parse the response and make the code 100% typesafe and reliable. 6 | 7 | To run the files, use [esbuild-kit/tsx](https://github.com/esbuild-kit/tsx): 8 | 9 | ```bash 10 | npx tsx src/not-typesafe.ts 11 | npx tsx src/using-unknown.ts 12 | npx tsx src/streamlining-with-zod.ts 13 | ``` 14 | 15 | If I'm doing something wrong, please let me know! -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "safe-fetch-calls-in-typescript", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "typescript": "^4.9.5" 15 | }, 16 | "dependencies": { 17 | "zod": "^3.20.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | typescript: ^4.9.5 5 | zod: ^3.20.2 6 | 7 | dependencies: 8 | zod: 3.20.2 9 | 10 | devDependencies: 11 | typescript: 4.9.5 12 | 13 | packages: 14 | 15 | /typescript/4.9.5: 16 | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} 17 | engines: {node: '>=4.2.0'} 18 | hasBin: true 19 | dev: true 20 | 21 | /zod/3.20.2: 22 | resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==} 23 | dev: false 24 | -------------------------------------------------------------------------------- /src/not-typesafe.ts: -------------------------------------------------------------------------------- 1 | type Airline = { 2 | id: number; 3 | name: string; 4 | country: string; 5 | logo: string; 6 | slogan: string; 7 | head_quaters: string; 8 | website: string; 9 | established: string; 10 | }; 11 | 12 | const getAirlinesById = async (id: number): Promise => { 13 | const data = await fetch( 14 | `https://api.instantwebtools.net/v1/airlines/${id}` 15 | ).then((res) => res.json()); 16 | 17 | return data as Airline; 18 | }; 19 | 20 | const airline = await getAirlinesById(2); 21 | 22 | console.log(airline.name); 23 | 24 | // false sense of type safety 25 | // this can break down very easily if the API changes 26 | -------------------------------------------------------------------------------- /src/streamlining-with-zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const airlineSchema = z.object({ 4 | id: z.number(), 5 | name: z.string(), 6 | country: z.string(), 7 | logo: z.string(), 8 | slogan: z.string(), 9 | head_quaters: z.string(), 10 | website: z.string(), 11 | established: z.string(), 12 | }); 13 | 14 | const getAirlinesById = async (id: number) => { 15 | const data = await fetch( 16 | `https://api.instantwebtools.net/v1/airlines/${id}` 17 | ).then((res) => res.json()); 18 | 19 | return airlineSchema.safeParse(data); 20 | }; 21 | 22 | const airline = await getAirlinesById(2); 23 | 24 | if (airline.success) { 25 | console.log(airline.data.name); 26 | } else { 27 | console.log("Airline not found"); 28 | } 29 | -------------------------------------------------------------------------------- /src/using-unknown.ts: -------------------------------------------------------------------------------- 1 | type Airline = { 2 | id: number; 3 | name: string; 4 | country: string; 5 | logo: string; 6 | slogan: string; 7 | head_quaters: string; 8 | website: string; 9 | established: string; 10 | }; 11 | 12 | const isAirline = (airline: unknown): airline is Airline => { 13 | return ( 14 | typeof airline === "object" && 15 | airline !== null && 16 | "id" in airline && 17 | "name" in airline && 18 | "country" in airline && 19 | "logo" in airline && 20 | "slogan" in airline && 21 | "head_quaters" in airline && 22 | "website" in airline && 23 | "established" in airline 24 | ); 25 | }; 26 | 27 | const getAirlinesById = async (id: number): Promise => { 28 | const data = (await fetch( 29 | `https://api.instantwebtools.net/v1/airlines/${id}` 30 | ).then((res) => res.json())) as unknown; 31 | 32 | if (!isAirline(data)) { 33 | return null; 34 | } 35 | 36 | return data; 37 | }; 38 | 39 | const airline = await getAirlinesById(2); 40 | 41 | if (airline !== null) { 42 | console.log(airline.name); 43 | } else { 44 | console.log("Airline not found"); 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 5 | "module": "esnext", 6 | "moduleResolution": "nodenext", 7 | "resolveJsonModule": true, 8 | "outDir": "./dist", 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "removeComments": true, 13 | "strict": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitOverride": true, 16 | "noImplicitReturns": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "useUnknownInCatchVariables": true, 20 | "noUncheckedIndexedAccess": true, 21 | "allowSyntheticDefaultImports": true, 22 | "esModuleInterop": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "skipLibCheck": true, 25 | "useDefineForClassFields": true 26 | } 27 | } 28 | --------------------------------------------------------------------------------