Skip to content

Expert · TypeScript basics · topic 55 of 64 · 40 min

TypeScript in real projects

Explanation

In a real project, tsconfig.json holds the settings: turn strict on. Type your DOM code and check for null, and never trust JSON from an API: treat it as unknown and check it. Use as and ! rarely, install @types packages for old libraries, and move a JavaScript project over one file at a time.

tsconfig.json: the project settings

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
A typical setup for a Vite app (tools like Vite create one for you)
OptionWhat it does
strictTurns on the strong checks, including strictNullChecks and noImplicitAny. Keep it on.
targetWhich JavaScript version the output uses; ES2022 keeps modern syntax
module, moduleResolutionHow import and export work; "bundler" fits Vite and similar tools
libWhich built-in APIs exist; "DOM" adds document, window and fetch
noEmitOnly check, write no .js files, because the bundler builds the code
allowJs, checkJsLet .js files join the project, and check them too

Typing DOM code

typescript
const input = document.querySelector<HTMLInputElement>("#pin");
if (!input) throw new Error("PIN input is missing");
input.value = "560001"; // OK: input is HTMLInputElement here

const form = document.querySelector("form"); // HTMLFormElement | null
form?.addEventListener("submit", (event) => {
  event.preventDefault(); // event is inferred as SubmitEvent
});

document.addEventListener("keydown", (e) => {
  console.log(e.key); // e is KeyboardEvent
});
querySelector can return null, so check it; event types are inferred

Do not trust API JSON

response.json() gives back any. Writing as Product[] just believes the server. If the server sends price as "120" (text) or forgets a field, the bug reaches your users. Treat the reply as unknown and check it.

Think of it like this: A kirana shop owner counts the packets in a delivery against the bill before signing. Writing as Product[] is signing without counting: if the delivery is short, you find out later, at the worst time. A check function is the counting.
typescript
async function loadProducts(): Promise<Product[]> {
  const res = await fetch("/api/products");
  if (!res.ok) throw new Error("HTTP " + res.status);
  const data: unknown = await res.json();
  if (!Array.isArray(data)) throw new Error("Expected a list");
  return data.filter(isProduct); // isProduct is a type guard
}

// With a schema library such as Zod, one description gives the check AND the type:
// const ProductSchema = z.object({ id: z.string(), name: z.string(), price: z.number() });
// type Product = z.infer<typeof ProductSchema>;
// const product = ProductSchema.parse(data); // throws if the shape is wrong
unknown + a check, or a schema library for bigger apps

Type assertions (as) and the non-null !

typescript
const total = document.getElementById("total") as HTMLSpanElement; // "trust me, it is a span"
const button = document.querySelector("button")!;                  // "trust me, it is not null"
// If you are wrong, the app crashes at runtime: TypeScript believed you.
as and ! change nothing at runtime; they only silence the compiler

Prefer a real check (if, a type guard) over as and !. Use as only when you truly know more than the compiler, for example just after you created the element yourself.

Types for libraries: .d.ts files and @types

A .d.ts file (a declaration file) contains only types, no code. Most modern npm packages ship their own. For older JavaScript libraries, the community DefinitelyTyped project publishes types as separate @types packages, installed with npm install -D @types/<name>. If VS Code says it could not find a declaration file for a module, look for an @types package first.

Moving a JavaScript project to TypeScript

  1. Add // @ts-check as the first line of a .js file: VS Code starts checking it, with no build change.
  2. Add a tsconfig.json with "allowJs": true, so .js and .ts files can live together.
  3. Rename one file at a time from .js to .ts (or .jsx to .tsx) and fix its errors.
  4. Turn strict on for new code; in a large old project, turn options on step by step.
  5. Replace any with real types as you go.

Reading common compiler errors

Error (short)What it meansUsual fix
TS2322: Type 'X' is not assignable to type 'Y'A value of the wrong type is storedFix the value, or the type if the type is wrong
TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'A function got the wrong kind of argumentConvert first, e.g. Number(text)
TS2339: Property 'x' does not exist on type 'Y'A typo, or the type does not have that fieldFix the spelling, add the field, or narrow first
TS18047: 'x' is possibly 'null'The value might be nullCheck with if, or use ?. and ??
TS7006: Parameter 'x' implicitly has an 'any' typeA parameter has no type under strictAdd a type to the parameter
Careful: Do not "fix" an error by adding any, as, ! or // @ts-ignore just to make the red line go away. That hides the bug instead of fixing it. If you really must, use // @ts-expect-error with a comment that says why.

Watch the lesson

TypeScript in real projects · 1/8

"strict": true

tsconfig dot json holds the project settings. Always turn strict on.

Prefer a video by a teacher? Find free videos on “typescript tsconfig strict mode migrate javascript to typescript” on YouTube ↗

In real life: A PIN code lookup form

A delivery app has a PIN code box. The TypeScript code gets the input with querySelector<HTMLInputElement>, checks it is not null, and reads the value on submit. It calls an API for the area name, takes the reply as unknown, and uses a type guard to check it has a string area and a boolean deliverable before showing "Delivery available in Koramangala". When the API team renamed area to areaName, the check caught it on the first test run instead of showing "undefined" to customers.

Practice (0/3 done)

Task 1 · Practice

Convert a small JavaScript file to TypeScript. Take a file from one of your earlier projects (for example the shopping cart or the form code from the DOM module). First add // @ts-check at the top and fix what VS Code reports. Then add a tsconfig.json with "strict": true and "allowJs": true, rename the file to .ts, add types for the parameters and the main objects, and run npx tsc --noEmit until it prints no errors.

Task 2 · Code · Typescript

type Product = { id: string; name: string; price: number }. Write a type guard isProduct(value: unknown): value is Product that returns true only when value is an object (not null) whose id and name are strings and whose price is a number. isProduct({ id: "p1", name: "Tea", price: 40 }) is true; isProduct({ id: "p1", name: "Tea", price: "40" }) is false; isProduct(null) is false.

Task 3 · Code · Typescript

Write parseProducts(json: string): Product[]. It parses the JSON text. If the text is not valid JSON, or the result is not an array, return []. Otherwise return only the items that pass isProduct (already written for you in the starter). For the text of [{ "id": "p1", "name": "Tea", "price": 40 }, { "id": "p2", "name": "Milk" }] it returns only the tea product.

Quick quiz

Question 1

1.In a Vite project, why does tsconfig.json often have "noEmit": true?

Question 2

2.const input = document.querySelector<HTMLInputElement>("#pin"); What is the type of input?

Question 3

3.const data = (await res.json()) as Product[]; What is the risk?

Question 4

4.You install an old JavaScript library and VS Code says it could not find a declaration file for the module. What should you try first?

Question 5

5.The error says: Parameter 'price' implicitly has an 'any' type. What is the usual fix?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Generics and utility types