TypeScript 01 · TypeScript basics
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…
TypeScript 01 · TypeScript basics
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 types in the lessons that follow won’t catch you out.
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) | const data: User = await res.json() | Parameters of inline handlers in JSX |
| Data from an API (JSON) | 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)
let selected = null // later: selected = anna (anna is a User)
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 minMax(nums: number[]) {
return [Math.min(...nums), Math.max(...nums)]
}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.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
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. Which types go where is in the comments in the code.
Task is in the comments in the code below – at the top of the file and at the places marked TODO.
Loading the interactive part…
The typical places in a React component: props, a callback, useState and an event handler. Which type goes where is in the comments in the code.
Medium and hard challenges unlock once you sign in. Like the whole course, they are free – just an e-mail, no password and no payment.
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.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).