TypeScript Quick Reference
TypeScript adds static type checking to JavaScript. The course uses it in browser components, API contracts, service and repository boundaries, and tests. This page is a reference for the recurring patterns.
Type information helps development tools find incompatible code before execution. It does not authenticate a user, validate arbitrary network input, enforce a database constraint, or prove runtime behavior.
TypeScript does not work in the browser console. It must be compiled to JavaScript before running. The course framework handles that compilation; you do not need to install a TypeScript compiler yourself.
Each code block is a separate example, so some type names are deliberately reused. For a first pass, focus on inference, object and array types, unions and nullability, function types, narrowing unknown, the limits of assertions, and running the type check. Use the modeling and type-derivation sections when those patterns appear later in the course.
Inference and annotations
TypeScript often infers a type from a value:
const courseName = "Web Software Development"; // stringconst attempts = 2; // numberconst published = false; // booleanAdd annotations where they make an interface or boundary clearer:
type Room = { name: string; capacity: number;};
const canFit = (room: Room, people: number): boolean => { return room.capacity >= people;};Avoid annotating every local variable. Inference keeps an implementation readable while explicit parameter and return types document important boundaries.
Object types, optional properties, and interfaces
A type alias can describe the required shape of an object:
type Listing = { id: number; title: string; description?: string; readonly createdAt: string;};description?: string means that the property may be absent. readonly prevents assignment through this TypeScript type; it does not make a runtime object deeply immutable.
An interface can describe a similar object contract:
interface ListingRepository { findById(id: number): Promise<Listing | null>; save(listing: Listing): Promise<void>;}The course uses whichever form best expresses the local design. Do not create competing declarations for the same boundary merely because both syntaxes exist.
Arrays, unions, and nullability
Array element types can be written in two equivalent common forms:
const ids: number[] = [1, 2, 3];const names: Array<string> = ["Aino", "Lee"];A union describes a value that can be one of several types:
type Identifier = number | string;
const displayIdentifier = (id: Identifier): string => { return typeof id === "number" ? `#${id}` : id.toUpperCase();};Listing | null requires code to handle a missing listing explicitly:
const findTitle = async ( repository: ListingRepository, id: number,): Promise<string | null> => { const listing = await repository.findById(id);
if (listing === null) { return null; }
return listing.title;};Optional chaining, such as listing?.title, is useful only when propagating undefined is actually the intended behavior. It should not hide a missing-value case that requires a response or state transition.
Function types and asynchronous results
Function parameters and return values can form an explicit contract:
type FindListing = (id: number) => Promise<Listing | null>;
const findListing: FindListing = async (id) => { // The implementation would query a repository or call an API. return id === 1 ? { id, title: "Desk", createdAt: "2026-01-01" } : null;};An async function returns a promise. If the awaited result is a Listing, the function’s return type is Promise<Listing>, not Listing.
Callbacks can be typed directly:
const selectListings = ( listings: Listing[], keep: (listing: Listing) => boolean,): Listing[] => { return listings.filter(keep);};Narrowing unknown
Use unknown for a value whose type has not yet been established, such as parsed JSON from an external request. Code must narrow the value before using it:
type Room = { name: string; capacity: number;};
const decodeRoom = (value: unknown): Room => { if ( typeof value !== "object" || value === null || !("name" in value) || typeof value.name !== "string" || !("capacity" in value) || typeof value.capacity !== "number" ) { throw new Error("Invalid room data"); }
return { name: value.name, capacity: value.capacity };};any removes these checks and allows unchecked operations to spread. Prefer unknown at untrusted boundaries, then narrow manually for a small example or use the runtime schema introduced later in the course.
The above approach gets repetitive as the number of properties grows. A cleaner approach would be to separate validation into a type guard:
type Room = { name: string; capacity: number;};
const isRoom = (value: unknown): value is Room => { if (typeof value !== "object" || value === null) { return false; }
return ( "name" in value && typeof value.name === "string" && "capacity" in value && typeof value.capacity === "number" );};
const decodeRoom = (value: unknown): Room => { if (!isRoom(value)) { throw new Error("Invalid room data"); }
return value;};In the course, we will look at libraries that can generate these checks from a schema, so that the validation and type information remain connected.
Assertions are not validation
A type assertion tells the checker what to assume:
const listings = (await response.json()) as Listing[];It does not inspect the response at runtime. The code can still receive null, an object with missing fields, or an error document. Prefer narrowing or schema validation when data crosses an untrusted boundary.
The non-null assertion in element!.textContent similarly suppresses a possibility without checking it. Use an explicit condition unless the invariant has already been established and documented.
Errors and catch
Thrown values are not guaranteed to be instances of Error. Narrow a caught value before reading its properties:
try { await submitBid();} catch (error: unknown) { const message = error instanceof Error ? error.message : "An unknown failure occurred"; console.error(message);}A custom error can carry information that callers need to handle:
class ApiError extends Error { constructor( message: string, readonly status: number, readonly code: string, ) { super(message); this.name = "ApiError"; }}Do not replace every error with a new generic Error; doing so can discard the status, code, or cause needed for diagnosis and user-visible handling.
DOM values and events
DOM lookups can fail, so their types are often nullable:
const heading = document.querySelector<HTMLHeadingElement>("h1");
if (heading !== null) { heading.textContent = "Campus marketplace";}Event types describe the event itself, while the target element may need a more specific type:
const readTitle = (event: Event): string => { const input = event.currentTarget;
if (!(input instanceof HTMLInputElement)) { throw new Error("Expected a title input"); }
return input.value;};Discriminated unions
A shared literal property can identify the possible states of a value:
type LoadState = | { kind: "idle" } | { kind: "loading" } | { kind: "loaded"; listings: Listing[] } | { kind: "failed"; message: string };
const statusText = (state: LoadState): string => { switch (state.kind) { case "idle": return "Not started"; case "loading": return "Loading"; case "loaded": return `${state.listings.length} listings`; case "failed": return state.message; }
const unreachable: never = state; return unreachable;};Within each branch, TypeScript knows which fields exist. Assigning the remaining value to never also makes a newly added state produce a type error until the switch handles it.
Generics
A generic preserves a relationship between input and output types:
const first = <T>(values: T[]): T | undefined => { return values[0];};
const firstId = first([10, 20]); // number | undefinedconst firstName = first(["Aino", "Lee"]); // string | undefinedThe type parameter T does not mean “any value with no checking.” It means that callers choose a type and the implementation must preserve the stated relationship.
Some course examples write a generic arrow function as <T,> instead of <T>. The trailing comma can distinguish a generic parameter list from markup in JSX-like parsing contexts; it does not add another type parameter or change the function’s type.
Generic interfaces let the same abstraction carry different value types:
type Result<T> = | { ok: true; value: T } | { ok: false; message: string };Literal types, as const, and keys
Normally, an object property such as status: 201 may be inferred as the broad type number. as const preserves literal values:
const responseCodes = { created: 201, notFound: 404, conflict: 409,} as const;
type ResponseCode = (typeof responseCodes)[keyof typeof responseCodes];// 201 | 404 | 409typeof responseCodes obtains the static type of the value. keyof obtains its property-name union. Their combination is useful when code and types must remain connected to one declared table.
Use as const to preserve an accurate literal relationship.
Deriving and importing types
TypeScript can derive a function’s return type without calling it:
const createRoutes = () => { return { health: "/health", listings: "/listings", } as const;};
type Routes = ReturnType<typeof createRoutes>;ReturnType<typeof createRoutes> means “the type returned by this function.” The course uses the same idea to preserve route information from a completed Hono application.
Import a declaration without creating a runtime dependency by using import type:
import type { AppType } from "./generated/api-types.ts";Type-only imports are erased from emitted JavaScript. This distinction is important at the client/server boundary: a browser can receive public type information without importing API startup code, database libraries, or credentials.
Run the actual type check
A successful JavaScript or TypeScript transpilation is not necessarily a successful type check. Run the check command specified by the supplied project or activity. In the client container used later in the course, deno task check combines Svelte and contract checks; a Vite build alone does not establish the same property.
Keep type checking, runtime tests, and builds conceptually separate. Each can fail for a different reason and provides different evidence.
Where this reference leads
Part 1 uses inferred and explicit types in the supplied application, and Part 2 applies them in browser components. Part 3 adds narrowing and runtime schemas at API boundaries. Part 4 builds on generics, literal types, derived route types, and type-only imports when introducing typed HTTP clients. Return to the JavaScript Quick Reference when the runtime language behavior, rather than its static description, is the unfamiliar part.