Tutorial
TypeScript reference
The subset I actually type on a consultant desk. Strict mode, unions, generics you can read, and Next.js static export.
TypeScript is JavaScript with names for the shapes. The compiler is a linter that runs before the browser. This site is strict: true. That is the default you want on a client project.
I write C# when the server needs a spine. I write TypeScript when the page has to ship as files. The two habits transfer: name the type, keep the function small, fail in one place.
A file that is enough
export interface Person {
name: string
email: string
hire: boolean
}
export function byline(person: Person): string {
return `${person.name} <${person.email}>`
}
interface is a shape. string is a string. The function says what it takes and what it returns. If a caller passes a number for name, the build stops. That is the whole pitch.
Strict
tsconfig.json on this site:
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"moduleResolution": "bundler",
"jsx": "react-jsx",
"paths": { "@/*": ["./*"] }
}
}
strict turns on strictNullChecks, noImplicitAny, and the rest. noEmit is for Next.js: the bundler emits, tsc only checks. @/* is why import { person } from "@/lib/site" works.
any is a hole. Use unknown when you do not know yet, then narrow.
function asError(value: unknown): string {
if (value instanceof Error) return value.message
return String(value)
}
Unions instead of flags
type Status = "idle" | "loading" | "ready" | "error"
function label(status: Status): string {
switch (status) {
case "idle":
return "Idle"
case "loading":
return "Loading"
case "ready":
return "Ready"
case "error":
return "Error"
}
}
A boolean isLoading plus hasError is four states you did not name. A union is four states you did. The switch is exhaustive under strict. Forget a case and the build stops.
Optional, not nullable soup
type Link = {
title: string
href: string
external?: boolean
}
external? is boolean | undefined. Check it, or default it.
function target(link: Link): "_blank" | undefined {
return link.external ? "_blank" : undefined
}
Do not write string | null | undefined unless a third-party API already did.
Generics you can read
A generic is a hole with a name. Keep the name short and obvious.
export function first<T>(items: T[]): T | undefined {
return items[0]
}
If you cannot say the name out loud (TItem, TKey), the generic is doing too much. Two type parameters is usually the ceiling on a page like this.
Next.js, static
This site is output: "export". That means:
- No server functions.
- No
headers()orcookies(). generateStaticParamsfor every dynamic route.trailingSlash: trueso GitHub Pages findsblog/foo/index.html.basePath: "/mira"in production, empty innext dev.
const basePath = process.env.GITHUB_PAGES === "true" ? "/mira" : ""
const nextConfig = {
output: "export",
trailingSlash: true,
basePath,
images: { unoptimized: true },
}
images.unoptimized is required on GitHub Pages. There is no image optimizer process. Prefix public files yourself when you use a raw <img>:
export function withBase(path: string): string {
const normalized = path.startsWith("/") ? path : `/${path}`
return `${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}${normalized}`
}
next/link prefixes href for you. A raw <a href="/feed.xml"> does not. Use withBase for those.
C# next to TypeScript
The same idea in C#:
public sealed record Person(string Name, string Email, bool Hire);
public static string Byline(Person person) =>
$"{person.Name} <{person.Email}>";
Records, unions (record + inheritance, or OneOf if you must), and nullability (string?) are the transfer. I use C# for services, tools, and Qt-adjacent work. I use TypeScript for the page you are reading.
Before you ship
strictis on.- No
anyin the files you own. - Dynamic routes have
generateStaticParams. - The production build is
GITHUB_PAGES=true npm run build, andout/is what GitHub serves. - You opened
/mira/as well as/, because the prefix is how project pages work.