Lesson 11 · Ecosystem
Where the type goes (TypeScript syntax)
When a colon, when <…>, when nothing. The anatomy of the syntax, a cheat sheet of situations and a fill-in exercise.
Loading lesson…
Lesson 11 · Ecosystem
When a colon, when <…>, when nothing. The anatomy of the syntax, a cheat sheet of situations and a fill-in exercise.
Loading lesson…
When you move house, you stick labels on the boxes: "kitchen – glass", "books", "winter clothes". The boxes themselves don't care about the label – the things inside are still the same. But the movers can tell from the labels what mustn't go under heavy books and where to take what. And once everything is unpacked, you peel off the labels and throw them away.
TypeScript is labels on JavaScript. You stick them on variables, parameters and properties: "there's a number here", "there's a user or nothing here". The code doesn't work any differently because of them – but the editor and the compiler can tell from them when you try to put glass under books (pass text where a number should be). And when compiling, all the labels are peeled off: the browser gets clean JavaScript.
This lesson isn't about which labels exist, but where they're stuck: when a colon, when <…> and when nothing. Once you have that down, the following lesson on TypeScript in React will be a piece of cake.
A see-through box doesn't need a label – you can see what's in it. In the same way, with const count = 0 TypeScript sees the zero and knows by itself it's a number. This is called type inference. You have to stick a label where you can't see inside: on an empty box (an empty array), on a box that's still standing empty for now (null), or on a box that's only on its way (a function parameter, data from an API).
The second thing worth knowing: types disappear after compiling – the labels are peeled off. When you mentally erase all the types from the code, valid JavaScript must remain. That's the best test of whether a type is in the right place.
Let's try it on a typical function:
function formatPrice(amount: number, currency?: string): string {return amount + ' ' + (currency ?? 'USD')}
amount: number – the parameter amount is of type number. Name, colon, type.currency?: string – the question mark belongs to the name: the parameter is optional. Inside the function it can be undefined, hence ?? 'USD'.): string – a colon after the parameter brackets is the type of what the function returns. The same rule, only this time the "name" is the whole function call.Now let's peel the labels off:
function formatPrice(amount, currency) {return amount + ' ' + (currency ?? 'USD')}
Valid JavaScript remains. If it didn't – say if ({ title: string }) left behind something that does a different thing – the type was in the wrong place. We'll get to that in the next section.
What you see: Several typical lines with types. Each piece is coloured: purple for a type, orange for TypeScript syntax (colons, question marks, brackets), normal for plain JavaScript. Underlined pieces have an explanation.
Try it:
price, the colon and number in turn. Under the code you'll see what each piece means.The takeaway: A type is always a label stuck on a specific place, most often after a colon. Without it, JavaScript remains that does the same thing.
Loading the interactive part…
The colon exists in JavaScript even without TypeScript – in objects it separates the key and the value, in destructuring it renames. That's why it's the most common source of confusion: the same character, three different meanings. It helps to ask: is the colon after something I'm declaring right now? Then it's a label.
| Place | Syntax | Note |
|---|---|---|
| A variable | let name: string | Only when TS doesn’t infer the type from the value. |
| A parameter | (name: string) => … | Always. |
| An optional parameter / property | name?: string | The question mark belongs to the name. |
| A return value | function f(): string | After ), in an arrow function before =>. |
| A property in an interface / type | { name: string } | Describing the shape of an object. |
| Component props | ({ name }: Props) | After the whole destructuring! |
| There is NO colon here | Syntax | Why |
|---|---|---|
| A type definition | type Id = string | number | You’re naming a type → an equals sign. |
| An interface definition | interface User { … } | Keyword + name + body. |
| A type for a function/hook | useState<User | null>(null) | The type is PASSED to the function in angle brackets. |
| An object with values | { name: 'Anna' } | Here the colon separates the key and the VALUE. |
| JSX | <Card title="Hello" /> | No types in JSX – TS checks them against the props. |
| Destructuring | { title: heading } | Here the colon means RENAMING the variable. |
Back to the boxes. You stick a label where you can't see inside, and on boxes that arrive from outside. You don't stick one on a see-through box – you'd only add work and the risk that the label won't match the contents.
// See-through boxes – TS sees inside, don't write a labelconst count = 0 // numberconst names = ['Anna', 'Peter'] // string[]const [open, setOpen] = useState(false) // boolean// Boxes arriving from outside – always write a labelfunction greet(name: string) { … } // a parameter: who knows what someone will send hereinterface CardProps { title: string } // props: the same for components// Opaque boxes – TS can't see inside, add a labelconst [user, setUser] = useState<User | null>(null) // null now, a User laterconst [todos, setTodos] = useState<Todo[]>([]) // an empty array – of what items?
A simple rule: write types at the boundaries – where data comes in (parameters, props, APIs) or where TS has nothing to work out the type from. Inside functions, let it do its work.
| ✅ Always write | 🤔 Write when TS doesn’t infer | 🚫 Don’t write (unnecessary) |
|---|---|---|
| Function parameters | useState<User | null>(null) | const x: number = 5 |
| Component props (interface) | useState<Todo[]>([]) | useState<number>(0) |
| Data shapes (interface User) | useRef<HTMLDivElement>(null) | Parameters of inline handlers in JSX |
| The type of a context value | Standalone event handlers | Callback parameters (.map(item => …)) |
| Public library functions / utils | [object Object] assigned later | The return type of simple functions |
A cheat sheet for the situations you'll meet most often. Use it like a cookbook: find what you're writing right now and look up where the label goes.
What you see: Buttons with typical situations, split into plain JavaScript and React. For each situation there's the right syntax, the most common mistake and a short explanation.
Try it:
{ title: string } inside the brackets – you'll see it in the quiz in a moment too.The takeaway: Most situations repeat. Once you've looked them up in the cheat sheet a few times, you'll start writing labels in the right places automatically.
Loading the interactive part…
| Symbol | Example | Meaning |
|---|---|---|
: | age: number | "is of type" |
? | age?: number | optional (can be missing / undefined) |
| | string | null | "or" – a union |
& | A & B | "and at the same time" – combining types |
[] | string[] | an array |
[A, B] | [string, number] | a tuple – an array with a fixed length |
=> | (id: number) => void | a function type |
<T> | Array<User> | a generic – a type as a parameter |
as | data as User | "trust me" – a cast without a check (use sparingly) |
as const | ['a', 'b'] as const | the most precise (readonly) type possible |
! | ref.current! | "definitely not null" – without a check |
typeof | typeof config | a type from an existing value |
keyof | keyof User | a union of property names: "name" | "age" |
ChangeEvent) jumps to its definition.any offers "Infer parameter types from usage".Fill in the boxes with what belongs there – a type, a symbol, or nothing. Each exercise trains one place where a label goes. Remember the see-through boxes: sometimes the right answer is to leave the box empty.
function double(n) {
return n * 2
}function greet(name: string) {
return 'Hello ' + name
}function greet(name: string, excited boolean) {
return excited ? name + '!' : name
}interface User {
name string
age number // optional
}interface User { name: string }
const anna: User = { name: }type Status 'idle' | 'loading' | 'error'
interface AvatarProps {
src: string
size?: number
}
function Avatar({ src, size = 40 }) {
return <img src={src} width={size} />
}interface ListProps {
items: string[]
onSelect: // gets the index of the selected item
}const [user, setUser] = useState(null)
const [todos, setTodos] = useState([])
const [count, setCount] = useState(0)
const inputRef = useRef(null)
<input ref={inputRef} />const handleChange = (e) => {
setName(e.target.value)
}async function loadUser(id: number): {
const res = await fetch('/api/users/' + id)
return res.json()
}function first(items: T[]): T | undefined {
return items[0]
}function useToggle() {
const [on, setOn] = useState(false)
return [on, () => setOn(!on)]
}function Badge({ label: string, color: string }) {return <span style={{ color }}>{label}</span>}
setItems([...items, 'new']) report an error?const [items, setItems] = useState([])
// Aconst handleClick: (e: MouseEvent<HTMLButtonElement>) => void = (e) => { … }// Bconst handleClick = (e: MouseEvent<HTMLButtonElement>) => { … }// Cconst handleClick = (e) : MouseEvent<HTMLButtonElement> => { … }
const Card = ({ title }) => <h2>{title}</h2>
Swap any for a real type twice and fill in what the functions return.
CHALLENGE: Types instead of any The functions have the type `any` on their parameters and return nothing. 1. formatPrice: change `any` to `number` and return text with the currency: formatPrice(25) → '$25' 2. isAdult: change `any` to `number`, add the return type `: boolean` after the bracket and return age >= 18 The tests check the results. `npm run typecheck` checks the types when you have the course downloaded.
Loading the interactive part…
In both challenges the finished code is full of any – boxes without labels. Your task is to stick the labels in the right places: first in plain functions, then in a React component. Nothing changes on the page; only TypeScript sees the difference.
Seven small functions, seven places where a type goes. The output on the page won't change – only TypeScript sees the difference.
CHALLENGE: Where do the types go? – plain functions
Every function below has `any`. Replace them with the right types. This isn't about React – it's about
WHERE a type goes: after a parameter, after the parameter brackets, into an interface, into <T>.
1. interface Person: firstName (string), lastName (string), age (an OPTIONAL number).
Then replace `export type Person = any` with `export interface Person { … }`.
2. formatPrice(amount, currency) – amount is a number, currency is 'USD' | 'EUR'
with the default value 'USD'. Returns a string.
3. sum(numbers) – an array of numbers → number.
4. parseAge(input) – string → number | null (null when it isn't a number).
5. fullName(person) – Person → string.
6. adults(people) – Person[] → Person[] (only those with age >= 18).
7. findById(items, id) – GENERIC: works for any array of objects with `id: number`
and returns the item's type (or undefined). Tip: <T extends { id: number }>
✅ Check: npm run typecheck:challenges (the Utils.en.typetest.tsx file)Loading the interactive part…
All the typical places in a React component: props, a callback, useState, useRef, an event handler and a custom hook.
Medium and hard challenges are a bonus for signed-in readers. Signing up is free – just an e-mail, no password.
function f(): T.({ a, b }: Props). Inside { } the colon renames!type X = … (an equals sign), interface X { … }, a type for a hook in angle brackets: useState<T>().null, empty arrays); let TS infer the see-through boxes.Checks, Check)Checks, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}
products)products – from the file src/course/fakeApi.en.ts. The course fake API: data and functions pretending to be a server (with a delay, sometimes even with an error). In a real app you would call fetch() here.export interface Product {id: numbername: stringcategory: 'fruit' | 'vegetables' | 'bakery' | 'dairy'price: numberinStock: boolean}export const products: Product[] = [{ id: 1, name: 'Apple', category: 'fruit', price: 12, inStock: true },{ id: 2, name: 'Banana', category: 'fruit', price: 8, inStock: true },{ id: 3, name: 'Pear', category: 'fruit', price: 15, inStock: false },{ id: 4, name: 'Carrot', category: 'vegetables', price: 6, inStock: true },{ id: 5, name: 'Tomato', category: 'vegetables', price: 9, inStock: true },{ id: 6, name: 'Cucumber', category: 'vegetables', price: 19, inStock: false },{ id: 7, name: 'Bread roll', category: 'bakery', price: 3, inStock: true },{ id: 8, name: 'Bread', category: 'bakery', price: 45, inStock: true },{ id: 9, name: 'Milk', category: 'dairy', price: 22, inStock: true },{ id: 10, name: 'Cheddar', category: 'dairy', price: 39, inStock: true },{ id: 11, name: 'Yogurt', category: 'dairy', price: 14, inStock: false },{ id: 12, name: 'Orange', category: 'fruit', price: 11, inStock: true },]
row, stack, card, list, btn, input or muted are the course's ready-made styles in src/styles.css (row = side by side, stack = stacked, card = bordered box, muted = grey text).