Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Beginner 13 min read

TypeScript, as much as you need

The notations you keep running into in React code

In one sentence

TypeScript is JavaScript where you also write down the shape of your data. The editor then complains before the browser does.

You know JavaScript – so surprisingly little is missing here. A handful of notations covers almost everything that shows up in React projects.

Put another way

Types are not a second program. They are comments that get checked. Before anything runs they are stripped out – the browser executes plain JavaScript.

The basic form: value : type

basics.ts
let name: string = "Ada";
let age: number = 36;
let active: boolean = true;
let tags: string[] = ["react", "ts"];      // array of strings

// Most of the time you don't need this – TypeScript works it out:
let title = "Hello";       // automatically string
// title = 42;             // error: a number does not fit here

// Functions: what goes in, what comes out
function double(n: number): number {
  return n * 2;
}

const shorten = (text: string, max = 20): string =>
  text.length > max ? text.slice(0, max) + "…" : text;
Tip Less typing is better

Write types only at the edges: props, function parameters, data from an API. Inside, TypeScript figures it out itself (InferenceTypeScript works out the type by itself, without you writing it down.). const x: number = 5 is pure noise.

Describing objects

objects.ts
type User = {
  id: string;
  name: string;
  email?: string;      // the ? means: may be missing
};

// Combining two types:
type Admin = User & { rights: string[] };

// interface can do almost the same:
interface Product {
  id: string;
  price: number;
}

`type` or `interface`? For React props it barely matters. Rule of thumb: use type – it does everything interface does, plus more.

Union: “one of these”

A Union“One of these”: string | number means the value is either a string or a number. lists the allowed options. With fixed words it becomes a set of choices your editor autocompletes.

unions.ts
type Variant = "primary" | "ghost" | "danger";
type Id = string | number;

let v: Variant = "ghost";     // fine
// let w: Variant = "blue";   // error – not one of them
Tip The most useful pattern in React

Instead of three booleans (loading, hasError, done) that can contradict each other, describe the states individually. Then an impossible state cannot even be written down.

state.ts
type State =
  | { status: "loading" }
  | { status: "error"; text: string }
  | { status: "done"; data: string[] };

function show(s: State) {
  if (s.status === "loading") return "Loading …";
  if (s.status === "error") return s.text;         // text definitely exists here
  return s.data.length + " results";               // data definitely exists here
}
The shared status field tells TypeScript which variant you have – after the check it knows exactly which fields are available.
Good to know What this is called

The technical term is Discriminated unionSeveral variants sharing one identifying field – TypeScript uses it to tell which variant you currently have.For example { status: "loading" } or { status: "error", text: string }. Check status and TypeScript knows which fields exist.. And the narrowing-down through the check is called NarrowingTypeScript narrows a type down when you check it: after if (typeof x === "number") it knows x is a number.. You do not have to remember the names – the pattern, yes.

Ready-made helpers you will see a lot

utilities.ts
type User = { id: string; name: string; email: string };

Partial<User>              // every field optional     -> for updates
Pick<User, "id" | "name">  // only these two fields
Omit<User, "email">        // everything except email  -> very common
Record<string, number>     // object: text key, number value

keyof User                 // "id" | "name" | "email"
These helpers are called utility types – they build a new type out of an existing one.
In more depth: generics, as const, satisfies and unknown optional

Generics – a placeholder for a type

A GenericA placeholder for a type that is only decided when it is used – the <T> in Array<T>. is a variable for types. You already know them from useState<number>(0). You rarely write your own, but you should be able to read them.

generics.ts
// T is only decided at the call site
function first<T>(items: T[]): T | undefined {
  return items[0];
}

first([1, 2, 3]);      // T = number
first(["a", "b"]);     // T = string

// With a constraint: T must have an id field
function byId<T extends { id: string }>(items: T[], id: string) {
  return items.find((i) => i.id === id);
}

as const – freezing values

widened to string
const ROLES = ["admin", "user"];
// type: string[]

type Role = (typeof ROLES)[number];
// -> string  (useless)
stays exact
const ROLES = ["admin", "user"] as const;
// type: readonly ["admin", "user"]

type Role = (typeof ROLES)[number];
// -> "admin" | "user"  ✨

This is how you derive types from your data instead of maintaining both by hand.

satisfies – check without coarsening

satisfies.ts
type Theme = Record<string, string>;

const theme = {
  bg: "#0A0A0A",
  ink: "#FFFFFF",
} satisfies Theme;

theme.bg;      // the editor knows bg and ink
// theme.foo;  // error – does not exist

// With ": Theme" instead of "satisfies Theme", theme.foo would have slipped through.

unknown instead of any

any switches off checking – everywhere that value flows afterwards. unknown says “I don't know yet” and forces you to look first.

unknown.ts
try {
  risky();
} catch (err) {           // err is unknown, not Error!
  const text = err instanceof Error ? err.message : "Unknown error";
  console.error(text);
}
Careful Types are gone at runtime

TypeScript checks beforehand and is then thrown away. So data from an API is not automatically correct just because you called it User. To make that safe, validate at runtime – with Zod, for example.

Does it stick?

5 questions on this lesson. Wrong answers show up in your stats.

Now write it yourself

Describing states properly

The component shows a loading state – currently wired up with any and producing no output. Make each state produce the right display.

  • loading shows Loading …
  • error shows the text from text
  • done shows N results (N = length of data)
  • Bonus: replace any with a union of three variants
type State = any; // TODO: replace with a union of three variants

export function Display({ s }: { s: State }) {
  // TODO: return the right output for each state
  return <p>???</p>;
}

export default function App() {
  return (
    <div style={{ fontFamily: "system-ui", padding: 16 }}>
      <Display s={{ status: "loading" }} />
      <Display s={{ status: "error", text: "Server gone" }} />
      <Display s={{ status: "done", data: ["a", "b", "c"] }} />
    </div>
  );
}

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →