TypeScript and JavaScript · Beginner

TypeScript for Python developers

4 min read

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.

PythonTypeScript
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 dataclassinterface or type
Nonenull and undefined
async def and awaitasync 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

basics.ts
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");
shellsession
$ node basics.ts
{ high: 2, low: 1 }
[ 1, 3 ]
ASHA

countBy 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:

tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "types": ["node"]
  }
}

A file with two mistakes:

bad.ts
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());
shellsession
$ 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.

population.ts
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;
}
shellsession
$ node population.ts
2023 1,43,80,69,596
2022 1,42,54,23,212
2021 1,41,42,03,896

Note 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 a Promise object instead of the data, and often a confusing error later.

  • Using any. It switches type checking off. Use unknown and narrow it, or validate it.

  • || instead of ?? for defaults. 0 || 10 is 10; 0 ?? 10 is 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

  1. Add a closedAt?: string field to Ticket and write a function that returns the average number of tags per closed ticket. Make tsc pass with strict on.

  2. Change population to take a list of countries and fetch them in parallel with Promise.all.

Further reading