TypeScript for AI Engineers: The Language of AI Web Apps A tutorial published for AI engineers shows Python programmers how to build a typed, tested TypeScript client for a chat API, with streaming, using Node 22 and TypeScript 6.0.3. The guide walks through installing typescript, tsx and @types/node, configuring tsconfig.json with "strict": true, and separating `npx tsx` to run files from `npx tsc --noEmit` to type-check them, noting the React, Next.js and ESLint ecosystem sections were described from knowledge rather than installed. It argues the browser should never hold the model API key, so a server in TypeScript or Python sits between the chat UI and the model API. You can build a great AI career in Python alone. But people use web pages, and the chat boxes, dashboards, and prompt playgrounds on those pages are usually built with TypeScript . Learn it, and you can ship the whole product yourself. This post is core. It's a fast path for Python programmers, not a full course, and by the end you'll have written a typed, tested client for the API from Post 20, streaming included. Everything here ran on Node 22 and TypeScript 6.0.3, so the outputs and error messages are real. The exception is the ecosystem section React, Next.js, ESLint , which I describe from knowledge because I couldn't install those here. Why TypeScript, and what it actually is JavaScript is the language that runs in every web browser, and, through a program called Node.js , on servers too. It's flexible and a bit loose, which is great for small scripts and painful for large projects. TypeScript is JavaScript plus types . You write type annotations, much like the type hints from Post 10, and a tool called the compiler tsc checks them and catches mistakes before your code runs. Then the types are erased , leaving plain JavaScript that runs anywhere. That last detail has big consequences, and we'll get to them later. Where does it fit in an AI app? Here's the usual shape: php browser TypeScript/React -- your server TypeScript or Python -- model API the chat screen holds the API key, adds logic the model The browser should never hold your API key Post 20 , so your server sits in the middle, keeps the secrets, and streams results back. That server can be TypeScript or Python, and the pieces talk to each other over HTTP, which you already understand. Setting up You need Node.js get the LTS version from nodejs.org . Check it: node --version npm --version npm is Node's package manager. It does for JavaScript what pip and uv do for Python. Create a project: mkdir chat-api-client cd chat-api-client npm init -y npm install --save-dev typescript tsx @types/node That installs three development tools: - typescript is the compiler and type checker. - tsx runs TypeScript files directly, without a separate build step. It's the easiest way to run TS while learning. - @types/node teaches TypeScript about Node's built-in features. Notice a folder called node modules/ appears, full of installed packages, along with package.json your project's description and dependencies, like pyproject.toml from Post 12 and package-lock.json the exact versions installed, like uv.lock . Commit the lock file, and add node modules/ to your .gitignore Post 22 . node modules can be enormous, and anyone can rebuild it by running npm install . Add "type": "module" to package.json , so your files use the modern import syntax. And create a tsconfig.json in the project folder to configure the compiler: { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noEmit": true, "skipLibCheck": true, "lib": "ES2022", "DOM", "DOM.Iterable" , "types": "node" }, "include": "src" } The most important line is "strict": true . It turns on the strictest checks, including the one that catches null and undefined mistakes. Always use it. TypeScript's defaults change between versions, so spelling out your settings, as above, keeps behavior predictable. noEmit means "just check, don't produce files," since tsx handles running. Now you have two commands, and the difference between them is important: npx tsx src/hello.ts RUN the file. Does NOT check types. npx tsc --noEmit CHECK the types. Doesn't run anything. tsx strips the types and runs the code, even if there are type errors. tsc does the checking. Get in the habit of running the check regularly, and add both to package.json so they're easy to remember: "scripts": { "start": "tsx src/client.ts", "check": "tsc --noEmit", "test": "tsx --test src/ .test.ts" } Then npm run check and npm test do the work. Newer versions of Node can also run TypeScript files directly by stripping types, but the details depend on your Node version, so I'll stick with tsx here. JavaScript for Python programmers TypeScript is JavaScript, so you need the JavaScript basics first. Here's a translation table, and then the gotchas that bite Python programmers. | Python | TypeScript / JavaScript | |---|---| | x = 5 | const x = 5 or let x = 5 if it will change | | None | null or undefined two kinds of "nothing" | | True / False | true / false | | f"Hello, {name}" | Hello, ${name} backticks | | len items | items.length | | x in items | items.includes x | | for x in items: | for const x of items { ... } | | n n for n in nums if n % 2 == 0 | nums.filter n = n % 2 === 0 .map n = n n | | {"a": 1} dict | { a: 1 } an object , or a Map | | print x | console.log x | | def f x : ... | function f x { ... } or const f = x = ... | | try / except | try / catch | | async def / await | async function / await | | dataclass / Pydantic | interface or type , plus Zod for runtime checks | | pip / uv , pyproject.toml | npm , package.json | | pytest | Vitest, Jest, or Node's built-in test runner | I ran each of the following. Here they are, with the output. One number type. There's no separate int and float : console.log typeof 1, typeof 1.5, 0.1 + 0.2, 7 / 2, Math.floor 7 / 2 ; // number number 0.30000000000000004 3.5 3 Note that 7 / 2 is 3.5 , as in Python 3. For whole-number division, use Math.floor . The 0.1 + 0.2 quirk from Post 4 is the same. Two kinds of "nothing". null means "deliberately empty," and undefined means "never set." You'll see both: js let a: string | null = null; let b: string | undefined; console.log a, b, a == b, a === b ; // null undefined true false Always use === . The double-equals == converts types before comparing, with surprising results: console.log 0 == "", 0 === "", "1" == 1 ; // true false true 0 == "" is true . Don't use == . Happily, TypeScript flags many of these comparisons for you: it reports This comparison appears to be unintentional because the types 'number' and 'string' have no overlap. Truthiness differs from Python. An empty array and an empty object are truthy in JavaScript, while they're falsy in Python: console.log ? "empty array is truthy" : "falsy" ; // empty array is truthy console.log 0, "", null, undefined, NaN .map v = Boolean v ; // false, false, false, false, false The falsy values are false , 0 , "" , null , undefined , and NaN . To check for an empty array, test items.length === 0 . TypeScript even complains about ? ... , with This kind of expression is always truthy. ?? versus || . This one causes real bugs. || falls back for any falsy value, including a legitimate 0 . The "nullish coalescing" operator ?? falls back only for null and undefined : js const retries = 0; console.log "with ||:", retries || 3, "| with ??:", retries ?? 3 ; // with ||: 3 | with ??: 0 If someone sets retries to 0 on purpose, || quietly turns it into 3 . Use ?? for defaults. Its partner is optional chaining , ?. , which stops safely if something is missing: js const user: { profile?: { email?: string } } = {}; console.log user.profile?.email, user.profile?.email ?? "no email" ; // undefined no email Without ?. , user.profile.email would crash, as None.email does in Python. Array methods replace comprehensions: js const nums = 1, 2, 3, 4, 5, 6 ; console.log nums.filter n = n % 2 === 0 .map n = n n ; // 4, 16, 36 console.log nums.reduce sum, n = sum + n, 0 ; // 21 console.log nums.find n = n 3 , nums.some n = n 5 , nums.every n = n 0 ; // 4 true true The sorting gotcha. .sort with no argument sorts as text : console.log 10, 9, 1, 100 .sort ; // 1, 10, 100, 9 console.log 10, 9, 1, 100 .sort x, y = x - y ; // 1, 9, 10, 100 Always pass a comparison function for numbers. Destructuring and spread are lovely, and you'll use them constantly: const { role, content } = { role: "user", content: "hi", extra: 1 }; const first, ...rest = 1, 2, 3 ; const merged = { ...{ model: "x", temperature: 0.7 }, temperature: 0.2 }; console.log role, content, first, rest, merged ; // user hi 1 2, 3 { model: 'x', temperature: 0.2 } The spread ... copies an object's fields, and later fields win, so merged has temperature: 0.2 . That's the standard way to apply overrides to default settings. Objects as dictionaries, and Map . A plain object works for string keys. A Map is better when keys vary or when you add and remove a lot: js const counts: Record