TypeScript and JavaScript · Beginner
TypeScript for Python developers
Why it matters
TypeScript appeared in a third of the Forward Deployed Engineer job descriptions we read, and JavaScript in a quarter. When you work inside a customer's stack or with an SDK's examples, you meet it constantly. If you know Python, most concepts carry over; the differences are types checked before the code runs, and asynchronous code everywhere.
The concept
TypeScript is JavaScript with type annotations. The annotations are checked by the compiler tsc and then removed, so at runtime it is plain JavaScript. Node.js 22.18 and later can run .ts files directly by stripping the types, which makes experimenting easy.
| Python | TypeScript |
|---|---|
list[str] | string[] |
dict[str, int] | Record<string, number> |
Optional[str] | an optional field, name?: string |
Literal["low", "high"] | a union of literal types (see Priority below) |
TypedDict or dataclass | interface or type |
None | null and undefined |
async def and await | async function and await |
f"{x}" | `${x}` (template literals) |
Two operators you will see everywhere: ?. (optional chaining: stop and return undefined if the value is missing) and ?? (use a default only when the value is null or undefined).
Worked example: typed tickets
type Priority = "low" | "medium" | "high";
interface Ticket {
id: number;
subject: string;
priority: Priority;
tags: string[];
assignee?: string;
}
const tickets: Ticket[] = [
{ id: 1, subject: "Refund not received", priority: "high", tags: ["billing"] },
{ id: 2, subject: "Change delivery address", priority: "low", tags: ["account"], assignee: "asha" },
{ id: 3, subject: "Double charge on UPI", priority: "high", tags: ["billing", "upi"] },
];
function countBy<T>(items: T[], key: (item: T) => string): Record<string, number> {
const counts: Record<string, number> = {};
for (const item of items) {
const k = key(item);
counts[k] = (counts[k] ?? 0) + 1;
}
return counts;
}
const unassignedHigh = tickets.filter((t) => t.priority === "high" && !t.assignee).map((t) => t.id);
console.log(countBy(tickets, (t) => t.priority));
console.log(unassignedHigh);
console.log(tickets[1].assignee?.toUpperCase() ?? "unassigned");$ node basics.ts
{ high: 2, low: 1 }
[ 1, 3 ]
ASHAcountBy is generic: T is whatever type the list holds, so the same function works for any list. Arrow functions such as (t) => t.priority are the equivalent of Python's lambda, but can span several lines.
The compiler catches mistakes before they run
Node only strips types; it does not check them. Run tsc for that. With this tsconfig.json and "type": "module" in package.json:
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"strict": true,
"noEmit": true,
"erasableSyntaxOnly": true,
"types": ["node"]
}
}A file with two mistakes:
type Priority = "low" | "medium" | "high";
interface Ticket { id: number; priority: Priority; assignee?: string }
const t: Ticket = { id: 4, priority: "urgent" };
console.log(t.assignee.toUpperCase());$ npx tsc -p .
bad.ts(3,28): error TS2322: Type '"urgent"' is not assignable to type 'Priority'.
bad.ts(4,13): error TS18048: 't.assignee' is possibly 'undefined'.Both would be runtime bugs in plain JavaScript. strict: true is what makes the second check happen; always turn it on in new projects.
Async code and fetch
Network calls return a Promise. Mark the function async and await the result. fetch is built into Node.js.
interface Observation {
date: string;
value: number | null;
}
async function population(country: string, from: number, to: number): Promise<Observation[]> {
const url = `https://api.worldbank.org/v2/country/${country}/indicator/SP.POP.TOTL?format=json&date=${from}:${to}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`World Bank API returned ${response.status}`);
}
const [, rows] = (await response.json()) as [unknown, Observation[]];
return rows.map(({ date, value }) => ({ date, value }));
}
try {
const rows = await population("IND", 2021, 2023);
for (const row of rows) {
console.log(row.date, row.value?.toLocaleString("en-IN") ?? "no data");
}
} catch (error) {
console.error("Could not load data:", (error as Error).message);
process.exitCode = 1;
}$ node population.ts
2023 1,43,80,69,596
2022 1,42,54,23,212
2021 1,41,42,03,896Note the as [unknown, Observation[]]. That is a type assertion: you are telling the compiler what the JSON looks like, and it believes you without checking. If the API returns something else, the types are wrong and nothing warns you. The next module fixes this with runtime validation.
Common mistakes
Forgetting
await. You get aPromiseobject instead of the data, and often a confusing error later.Using
any. It switches type checking off. Useunknownand narrow it, or validate it.||instead of??for defaults.0 || 10is 10;0 ?? 10is 0.Trusting type assertions on external data. Types are erased at runtime; data from networks must be validated.
Interview angle
"What is the difference between interface and type?" Both describe object shapes; interfaces can be extended and merged, while type can also name unions and other types. "What is the difference between null and undefined?" Undefined means a value was never set (a missing optional field); null is an explicit "no value". Knowing ?. and ?? shows you have written real TypeScript.
Practice task
Add a
closedAt?: stringfield toTicketand write a function that returns the average number of tags per closed ticket. Maketscpass withstricton.Change
populationto take a list of countries and fetch them in parallel withPromise.all.