A TypeScript Tutorial for Command-Line AI Programs
This chapter is a focused TypeScript tutorial covering the language features you will encounter throughout this book. All examples are command-line programs, no browser, DOM, or UI framework is involved. If you are already comfortable with TypeScript you can skip this chapter and use it as a later as a reference.
We assume you have a working Node.js and tsx installation as described in the previous chapter.
Type Basics
TypeScript’s core value proposition is static typing. The compiler catches type errors before your code runs.
Primitive Types
Primitive types are the most basic building blocks of TypeScript’s type system. They represent simple, single values like strings, numbers, and booleans. You can either explicitly annotate their types or let TypeScript infer them automatically based on the assigned value.
1 // Explicit type annotations
2 const name: string = "scikit-learn";
3 const version: number = 1.5;
4 const isStable: boolean = true;
5
6 // Type inference, TypeScript figures out the type
7 const framework = "TensorFlow"; // inferred as string
8 const layers = 96; // inferred as number
Arrays and Tuples
Arrays and tuples allow you to group multiple values together. While arrays represent collections where all elements share the same type, tuples are fixed-length arrays where each index is associated with a specific, predetermined type. You can also destructure tuples to easily extract their individual elements.
1 // Arrays: all elements are the same type
2 const scores: number[] = [0.95, 0.87, 0.92];
3 const labels: string[] = ["cat", "dog", "bird"];
4
5 // Tuples: fixed length, each position has a specific type
6 const prediction: [string, number] = ["malignant", 0.94];
7 const [label, confidence] = prediction; // destructuring
Union Types
When a value could be one of several types:
1 function parseInput(value: string | number): number {
2 if (typeof value === "string") {
3 return parseFloat(value);
4 }
5 return value;
6 }
7
8 console.log(parseInput("3.14")); // 3.14
9 console.log(parseInput(2.718)); // 2.718
Interfaces and Type Aliases
Interfaces define the shape of objects. They are used extensively for API responses, configuration objects, and data structures.
1 // Interface for a data sample
2 interface DataSample {
3 features: number[];
4 label: string;
5 confidence?: number; // optional property
6 }
7
8 const sample: DataSample = {
9 features: [5.1, 3.5, 1.4, 0.2],
10 label: "setosa",
11 };
12
13 // Type alias, similar to interface but also works for unions
14 type Prediction = {
15 label: string;
16 score: number;
17 };
18
19 type Result = Prediction | { error: string };
Readonly and Utility Types
TypeScript provides utility types for common patterns:
1 // Readonly prevents mutation
2 interface ModelConfig {
3 readonly learningRate: number;
4 readonly epochs: number;
5 batchSize: number;
6 }
7
8 const config: ModelConfig = {
9 learningRate: 0.01,
10 epochs: 100,
11 batchSize: 32,
12 };
13 // config.learningRate = 0.1; // Error: cannot assign to readonly
14
15 // Partial makes all properties optional
16 function updateConfig(
17 current: ModelConfig,
18 updates: Partial<ModelConfig>
19 ): ModelConfig {
20 return { ...current, ...updates };
21 }
22
23 // Record creates an object type with specific key and value types
24 const metrics: Record<string, number> = {
25 accuracy: 0.95,
26 precision: 0.93,
27 recall: 0.97,
28 };
Functions
Typed Parameters and Return Values
In TypeScript, you should explicitly declare the types of a function’s parameters and its return value. This ensures that the function is called with the correct arguments and that the calling code correctly handles whatever value is returned, which is especially useful for mathematical and distance calculations.
1 function euclideanDistance(a: number[], b: number[]): number {
2 let sum = 0;
3 for (let i = 0; i < a.length; i++) {
4 sum += (a[i] - b[i]) ** 2;
5 }
6 return Math.sqrt(sum);
7 }
8
9 console.log(euclideanDistance([1, 2, 3], [4, 5, 6])); // 5.196...
Arrow Functions
Arrow functions are concise and commonly used for callbacks and short operations:
1 const sigmoid = (x: number): number => 1 / (1 + Math.exp(-x));
2
3 const relu = (x: number): number => Math.max(0, x);
4
5 // With arrays
6 const values = [1.5, -0.3, 2.1, -1.0];
7 const activated = values.map(relu); // [1.5, 0, 2.1, 0]
Generic Functions
Generics let you write functions that work with any type while preserving type safety:
1 function argMax<T>(arr: T[], compareFn: (a: T, b: T) => number): number {
2 let maxIdx = 0;
3 for (let i = 1; i < arr.length; i++) {
4 if (compareFn(arr[i], arr[maxIdx]) > 0) {
5 maxIdx = i;
6 }
7 }
8 return maxIdx;
9 }
10
11 const scores = [0.1, 0.7, 0.2];
12 const bestIdx = argMax(scores, (a, b) => a - b);
13 console.log(`Best class: ${bestIdx}`); // Best class: 1
Function Overloads
When a function has different behaviors based on input types:
1 function normalize(data: number[]): number[];
2 function normalize(data: number): number;
3 function normalize(data: number | number[]): number | number[] {
4 if (Array.isArray(data)) {
5 const max = Math.max(...data);
6 return data.map(x => x / max);
7 }
8 return data;
9 }
Async/Await and Promises
Almost every AI API call is asynchronous. TypeScript makes async code readable with async/await:
1 // A function that returns a Promise
2 async function fetchData(url: string): Promise<string> {
3 const response = await fetch(url);
4 if (!response.ok) {
5 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
6 }
7 return response.text();
8 }
9
10 // Using the async function
11 async function main() {
12 try {
13 const data = await fetchData("https://api.example.com/data");
14 console.log(data);
15 } catch (error) {
16 console.error("Failed to fetch:", error);
17 }
18 }
19
20 main();
Parallel Async Operations
When you need to make multiple independent API calls:
1 async function fetchMultiple(urls: string[]): Promise<string[]> {
2 // Promise.all runs all fetches in parallel
3 const responses = await Promise.all(
4 urls.map(url => fetch(url))
5 );
6 return Promise.all(responses.map(r => r.text()));
7 }
Typed API Responses
When making external network requests to AI APIs, the responses returned by fetch are untyped by default. You can define an interface that matches the expected JSON structure and cast the resolved response promise to that interface. This enables autocomplete and compile-time checks when accessing properties like token usage and generated text.
1 interface ChatResponse {
2 text: string;
3 model: string;
4 usage: {
5 promptTokens: number;
6 completionTokens: number;
7 };
8 }
9
10 async function chat(prompt: string): Promise<ChatResponse> {
11 const response = await fetch("https://api.example.com/chat", {
12 method: "POST",
13 headers: { "Content-Type": "application/json" },
14 body: JSON.stringify({ prompt }),
15 });
16 return response.json() as Promise<ChatResponse>;
17 }
Classes
Classes in TypeScript are useful for encapsulating state and behavior, we use them for models, agents, and data structures throughout this book.
1 class KNNClassifier {
2 private k: number;
3 private trainData: number[][] = [];
4 private trainLabels: string[] = [];
5
6 constructor(k: number = 5) {
7 this.k = k;
8 }
9
10 fit(data: number[][], labels: string[]): void {
11 this.trainData = data;
12 this.trainLabels = labels;
13 }
14
15 predict(sample: number[]): string {
16 // Calculate distances to all training samples
17 const distances = this.trainData.map((point, i) => ({
18 distance: this.euclidean(sample, point),
19 label: this.trainLabels[i],
20 }));
21
22 // Sort by distance and take the k nearest
23 distances.sort((a, b) => a.distance - b.distance);
24 const kNearest = distances.slice(0, this.k);
25
26 // Majority vote
27 const votes: Record<string, number> = {};
28 for (const { label } of kNearest) {
29 votes[label] = (votes[label] || 0) + 1;
30 }
31
32 return Object.entries(votes)
33 .sort(([, a], [, b]) => b - a)[0][0];
34 }
35
36 private euclidean(a: number[], b: number[]): number {
37 return Math.sqrt(
38 a.reduce((sum, val, i) => sum + (val - b[i]) ** 2, 0)
39 );
40 }
41 }
Abstract Classes
When you want to define a common interface that subclasses must implement:
1 abstract class BaseModel {
2 abstract train(data: number[][], labels: number[]): void;
3 abstract predict(sample: number[]): number;
4
5 evaluate(testData: number[][], testLabels: number[]): number {
6 let correct = 0;
7 for (let i = 0; i < testData.length; i++) {
8 if (this.predict(testData[i]) === testLabels[i]) {
9 correct++;
10 }
11 }
12 return correct / testData.length;
13 }
14 }
Modules and Imports
TypeScript uses ES modules. Each file is a module that explicitly exports and imports values.
Exporting
To share functions, classes, interfaces, or constants between files, you use the export keyword. Exported declarations can then be imported by other files in your project, promoting code reuse and logical separation.
1 // mathUtils.ts
2 export function dotProduct(a: number[], b: number[]): number {
3 return a.reduce((sum, val, i) => sum + val * b[i], 0);
4 }
5
6 export function vectorAdd(a: number[], b: number[]): number[] {
7 return a.map((val, i) => val + b[i]);
8 }
9
10 export const EPSILON = 1e-8;
Importing
You use the import keyword to bring exported variables, functions, or classes from other modules into the current file. In Node.js environment projects, you must specify the file path and include the appropriate file extension.
1 // main.ts
2 import { dotProduct, vectorAdd, EPSILON } from "./mathUtils.js";
3
4 const result = dotProduct([1, 2, 3], [4, 5, 6]);
5 console.log(result); // 32
Note the .js extension in the import path, this is required when using Node.js ES modules with TypeScript’s NodeNext module resolution.
Default Exports
A module can also designate a single value or class as its default export. Default exports can be imported without curly braces and can be renamed arbitrarily in the importing file, which is a common pattern when exporting a primary class.
1 // model.ts
2 export default class LinearRegression {
3 // ...
4 }
5
6 // main.ts
7 import LinearRegression from "./model.js";
Error Handling
TypeScript provides structured error handling that is essential when working with APIs and file I/O.
Try/Catch with Type Narrowing
When handling errors in asynchronous code or API calls, you can throw custom error classes that contain extra context like status codes. Within a catch block, you use the instanceof operator to narrow the type of the caught error, allowing you to selectively handle specific errors (like retrying transient network issues) while rethrowing others.
1 class ApiError extends Error {
2 constructor(
3 message: string,
4 public statusCode: number,
5 public retryable: boolean
6 ) {
7 super(message);
8 this.name = "ApiError";
9 }
10 }
11
12 async function callApi(prompt: string): Promise<string> {
13 try {
14 const response = await fetch("https://api.example.com/generate", {
15 method: "POST",
16 body: JSON.stringify({ prompt }),
17 });
18
19 if (!response.ok) {
20 throw new ApiError(
21 `API request failed`,
22 response.status,
23 response.status >= 500
24 );
25 }
26
27 const data = await response.json();
28 return data.text;
29 } catch (error) {
30 if (error instanceof ApiError && error.retryable) {
31 console.log("Retrying...");
32 return callApi(prompt); // simple retry
33 }
34 throw error;
35 }
36 }
Working with Files
Reading and writing files is common for loading datasets and saving results:
1 import { readFileSync, writeFileSync } from "node:fs";
2 import { readFile, writeFile } from "node:fs/promises";
3
4 // Synchronous, simple and fine for loading config/data at startup
5 const csvData = readFileSync("data.csv", "utf-8");
6 const lines = csvData.split("\n");
7
8 // Asynchronous, preferred for larger files or in async contexts
9 async function loadDataset(path: string): Promise<number[][]> {
10 const content = await readFile(path, "utf-8");
11 return content
12 .trim()
13 .split("\n")
14 .slice(1) // skip header
15 .map(line => line.split(",").map(Number));
16 }
17
18 // Writing results
19 async function saveResults(path: string, data: object): Promise<void> {
20 await writeFile(path, JSON.stringify(data, null, 2));
21 }
Parsing CSV Files
Since we frequently work with CSV data in AI projects:
1 interface CSVRow {
2 [key: string]: string;
3 }
4
5 function parseCSV(content: string): CSVRow[] {
6 const lines = content.trim().split("\n");
7 const headers = lines[0].split(",").map(h => h.trim());
8 return lines.slice(1).map(line => {
9 const values = line.split(",");
10 const row: CSVRow = {};
11 headers.forEach((header, i) => {
12 row[header] = values[i]?.trim() ?? "";
13 });
14 return row;
15 });
16 }
Enums and Literal Types
Useful for defining fixed sets of values:
1 // String enum for model types
2 enum ModelType {
3 Classification = "classification",
4 Regression = "regression",
5 Clustering = "clustering",
6 }
7
8 // Literal type, lightweight alternative to enums
9 type Activation = "relu" | "sigmoid" | "tanh" | "softmax";
10
11 function createLayer(units: number, activation: Activation) {
12 console.log(`Layer: ${units} units, activation: ${activation}`);
13 }
14
15 createLayer(128, "relu");
16 // createLayer(128, "linear"); // Error: not assignable to type Activation
Map, Set, and Iterators
Modern data structures that are useful throughout AI programming:
1 // Map for label counts (like Python's Counter)
2 function countLabels(labels: string[]): Map<string, number> {
3 const counts = new Map<string, number>();
4 for (const label of labels) {
5 counts.set(label, (counts.get(label) ?? 0) + 1);
6 }
7 return counts;
8 }
9
10 const labels = ["cat", "dog", "cat", "bird", "dog", "cat"];
11 const counts = countLabels(labels);
12 console.log(counts); // Map { 'cat' => 3, 'dog' => 2, 'bird' => 1 }
13
14 // Set for unique values
15 const uniqueLabels = new Set(labels);
16 console.log(uniqueLabels); // Set { 'cat', 'dog', 'bird' }
Practical Patterns for AI Code
Matrix Operations with Nested Arrays
Since we don’t use NumPy in TypeScript, we work with arrays directly:
1 type Matrix = number[][];
2 type Vector = number[];
3
4 function matMul(a: Matrix, b: Matrix): Matrix {
5 const rows = a.length;
6 const cols = b[0].length;
7 const inner = b.length;
8 const result: Matrix = Array.from({ length: rows },
9 () => new Array(cols).fill(0)
10 );
11
12 for (let i = 0; i < rows; i++) {
13 for (let j = 0; j < cols; j++) {
14 for (let k = 0; k < inner; k++) {
15 result[i][j] += a[i][k] * b[k][j];
16 }
17 }
18 }
19 return result;
20 }
21
22 function transpose(m: Matrix): Matrix {
23 return m[0].map((_, i) => m.map(row => row[i]));
24 }
25
26 function mean(v: Vector): number {
27 return v.reduce((a, b) => a + b, 0) / v.length;
28 }
29
30 function standardDeviation(v: Vector): number {
31 const avg = mean(v);
32 const squaredDiffs = v.map(x => (x - avg) ** 2);
33 return Math.sqrt(mean(squaredDiffs));
34 }
Configuration with Environment Variables
A pattern used in almost every AI project:
1 interface AppConfig {
2 googleApiKey: string;
3 openaiApiKey: string;
4 ollamaUrl: string;
5 model: string;
6 }
7
8 function loadConfig(): AppConfig {
9 const required = (name: string): string => {
10 const value = process.env[name];
11 if (!value) {
12 throw new Error(`Missing required environment variable: ${name}`);
13 }
14 return value;
15 };
16
17 return {
18 googleApiKey: required("GOOGLE_API_KEY"),
19 openaiApiKey: process.env.OPENAI_API_KEY ?? "",
20 ollamaUrl: process.env.OLLAMA_URL ?? "http://localhost:11434",
21 model: process.env.MODEL ?? "gemini-2.5-flash",
22 };
23 }
Progress Reporting for Long Operations
For long-running processes such as training loops or large batch predictions, providing real-time feedback is crucial. You can write a helper function that prints progress updates using process.stdout.write and standard escape sequences to overwrite the current terminal line.
1 function printProgress(current: number, total: number, label: string): void {
2 const pct = ((current / total) * 100).toFixed(1);
3 process.stdout.write(`\r ${label}: ${pct}% (${current}/${total})`);
4 if (current === total) process.stdout.write("\n");
5 }
6
7 // Usage in a training loop
8 for (let epoch = 0; epoch < 100; epoch++) {
9 // ... training logic ...
10 printProgress(epoch + 1, 100, "Training");
11 }
TypeScript Tutorial Wrap-up
This chapter covered the TypeScript features you will encounter throughout this book:
- Types and interfaces for defining data shapes and catching errors at compile time.
- Async/await for clean, readable API calls: essential for LLM and cloud service integration.
- Classes for encapsulating model logic, agents, and data structures.
- Modules for organizing code across files.
- Generics for writing reusable, type-safe utility functions.
- Error handling patterns for robust API interaction.
- File I/O for loading datasets and saving results.
- Practical patterns like matrix operations, configuration management, and progress reporting.
With these fundamentals in hand, you are ready to dive into the AI chapters. The next part covers machine learning with TypeScript.