A signature says what goes in and out
One line records the shape of every parameter and the shape of the result. The person who writes the function does not have to explain it, the person who calls it does not have to guess, and the compiler checks that both sides keep their side of the deal.
Reading a signature: five parts, one line
A delivery note lists what arrives and what leaves. A function signature does the same job for a function. Click each part.
Left of the colon is the parameter name, which is how the body refers to the value. Right of the colon is the type, which is the shape the caller has to provide. Pass something that is not a MenuItem and the call does not compile.
Start from the difference. Here is the same function twice. On the left you have to read the body to find out what it wants. On the right the first line already says it.
Parameter types are usually required: the compiler has nothing to read them from. The return type is usually not required, because the compiler reads it from the return statements.
So when is writing the return type still worth it?
Two situations. The first is a public boundary: a function that other modules, other teams, or a published package will call. There the annotation is the promise, and it stops an accidental change to the body from silently changing the type everyone depends on.
The second is that the error appears in a better place. With an annotation, a mistake is reported at the definition, on the line you are editing. Without one, the wrong type flows out and the error appears at some call site far away, or nowhere at all. The worst case is any:
Writing : Order puts a gate at the exit. The body may still infer any internally, but callers receive an Order, and a misspelled field is caught immediately.
Three ways to make a parameter flexible
Real functions do not require every argument every time. Optional parameters, default values, and rest parameters each have their own rules.
Optional and default are not the same thing
Both let the caller leave the argument out. The difference is what the parameter looks like inside the function. With topping?: string the type is string | undefined, so you have to check it before using it. With sugar = 50 the type is plain number: if the argument is missing, the default runs first, so undefined never reaches the body.
Passing undefined explicitly also triggers the default, which is why pourSugar("Milk Green", undefined) gives 50 too. When a parameter has a sensible default, prefer the default over ?: it removes a check from every line of the body.
An optional parameter cannot be followed by a required one
Arguments are matched by position. If an optional parameter sits in the middle, every required parameter after it can no longer be identified. The rule is: required first, then optional and defaulted ones.
A common question: is topping?: string the same as topping: string | undefined? No.
? controls whether the argument may be left out. | undefined controls which values the argument may hold. The second one still requires you to pass something.A rest parameter is typed as an array. It can also be typed as a tuple, which fixes the type of each leading position while still accepting a variable number of arguments after them. This is what makes variadic tuple types useful in practice.
Function types: a function has a shape too
Functions are passed around as values, so the type system needs a way to say which functions are acceptable.
Notice that the callback returns void. In a function type, void does not mean "you must return nothing". It means "the caller ignores the return value". A function that returns something is therefore still acceptable.
(…) => void rejected that, most forEach callbacks would have to be written with a block body just to throw the value away.void is a promise by the caller not to look. On a declaration, it is a requirement on the function itself. On a variable, it is an ordinary type that only undefined satisfies.Which function fits where another is expected
Once functions have types, the compiler needs a rule for comparing them. The rule for return types and the rule for parameter types run in opposite directions.
That direction is easy to accept: the caller asked for an Animal and got something that is an Animal plus more. Parameters work the other way, and this is where people get surprised.
Read it from the caller's side. Something holding a FeedAnimal may call it with any animal, including a cat. A function that reads d.breed would then fail. So the compiler rejects it. Checking parameters in this reversed direction is called contravariance, and it is switched on by the strictFunctionTypes flag, which strict turns on.
The exception: methods are still checked in both directions
strictFunctionTypes only applies to function type positions. A member declared with method syntax — feed(a: Animal): void rather than feed: (a: Animal) => void — keeps the older rule, where the parameter may go in either direction. That is called bivariance.
This is a known and deliberate unsoundness. It is not a rule that happens to be safe. The last two lines above compile and then put a plain Animal into an array that is really a Dog[]. TypeScript accepts it because rejecting it would break Array<Dog> being usable as Array<Animal>, along with a large amount of existing JavaScript. The team chose usability over soundness here, and documented the choice.
What to do with this: if you want the strict check on a member of an interface, declare it with property syntax. If you are wondering why an assignment you expected to fail was accepted, check whether method syntax is involved.
One more rule surprises almost everyone: a function with fewer parameters fits where one with more parameters is expected.
Why array.map(x => x) works
map calls its callback with three arguments, but you almost always write a callback that takes one. In JavaScript, extra arguments are simply ignored, so a callback that declares fewer parameters is always safe to call. The type system allows exactly what the language already allows.
The reverse is not safe. A callback that declares a fourth parameter would read an argument nobody passes, so the compiler rejects it.
Overloads and the this parameter
Two more things a signature can express. You will meet both while reading the types of third-party libraries.
Some functions behave differently depending on what you pass: give them a string and one thing comes back, give them an array and something else comes back. You can describe that with several overload signatures followed by one implementation signature.
Two rules people get wrong
First: the implementation signature is not callable from outside. It only has to be compatible with every overload above it. In the example, price is implemented for string | string[], but calling it with a string | string[] value is an error, because neither overload accepts that type.
Second: TypeScript picks the first overload that matches, in the order you wrote them. It does not look for the best match. So overload order is part of the API: write the more specific signatures first.
The other extra is a parameter named this. It is not a real parameter. It tells the compiler what this must be when the function runs.
this parameter is erased along with every other type, so the compiled JavaScript has a one-parameter function. It changes nothing at runtime. It only lets the compiler reject a call where this would be wrong.A signature can also narrow a type
A return type can be written as x is Dog or asserts x is Dog. Those are type predicates and assertion signatures: signatures that tell the compiler what a check has proved. They belong with narrowing, so chapter 03 covers them.
Object types: optional, readonly, index signatures
Chapter 01 gave objects a basic shape. Three modifiers extend it: which fields may be missing, which may not be changed, and what to do when the keys are not known in advance.
readonly is a compile-time check, not a lock
readonly stops you at compile time only. It leaves no trace in the emitted JavaScript, so at runtime the property can still be written. For a real runtime freeze you need Object.freeze. The compile-time check is still worth having: it catches the accidental writes, which are almost all of them.
Sometimes you cannot list the keys in advance. A stock table may get any SKU tomorrow. An index signature describes only the type of the keys and the type of the values.
When to use an index signature
If you can write the field names out, write them out. The compiler then checks your spelling. Use an index signature only when the keys are decided at runtime: SKUs, user input, a dictionary built from data. It is more permissive, and the cost of that is exactly the spelling check you just gave up.
interface vs type: a smaller difference than you have heard
Both describe the shape of an object, and in most cases they are interchangeable. Here is the syntax side by side, then the abilities that only one of them has.
The right side uses &, an intersection type. It is worth a closer look, because the next chapter contrasts it with unions.
never without an error at the declaration, so the problem only shows up later, at the place that tries to use the property. That is the practical reason to prefer extends when you are extending an object type.| Ability | interface | type | Notes |
|---|---|---|---|
| Describe an object shape | ✓ | ✓ | Most everyday code. Either one works. |
| Extend an existing shape | extends | & | extends reports a conflict at the declaration |
| Merge two declarations of the same name | ✓ only interface | ✕ duplicate identifier | declaration merging, used to extend global types |
Union ("s" | "m") | ✕ | ✓ only type | Only type can express one out of several |
| Mapped and conditional types | ✕ | ✓ only type | Type-level programming, chapters 06 and 07 |
So which one should you use?
The current advice in the TypeScript handbook is plain: pick either one and stay consistent within a codebase. If you need a union or a mapped type, only type can express it. If you are writing a library and want users to be able to extend a declaration, use interface. The claim that interface is always faster to compile is not a rule you should base a decision on.
Putting it together: the full makeOrder
One function that uses most of this chapter: a literal union, an interface, readonly, a default value, and an explicit return type.
toppings uses a default value instead of ?. Inside the body its type is Topping[], never undefined, so toppings.length needs no check.The signature is fixed. Now you play the compiler. For each of the six calls below, guess whether it compiles, then read what the compiler actually says.
makeOrder(milkTea, "large")makeOrder(milkTea)makeOrder(milkTea, "grande")makeOrder(milkTea, "medium", ["boba", "pudding"])makeOrder("Jasmine Milk Green", "large")makeOrder(milkTea, "large", "boba")This shop appears again
In chapter 03 this Order grows a status field: pending, paid, delivered. Written as a discriminated union, it lets the compiler work out which fields exist in each branch on its own.
Practice
Signatures are learned by writing them and reading the errors. Four tasks, all of which fit in the TypeScript Playground.
Quiz
Eleven questions on parameters, void, assignability, overloads, readonly, and interface vs type. A perfect score lights up the sidebar.
function brew(topping?: string, base: string) {} does not compile. Why?
type Cb = () => void; and then const f: Cb = () => 123; — what happens?
function load(json: string) { return JSON.parse(json); } — what is the risk here?
Which of these can only be written with type, not with interface? (choose all that apply)
What happens to a readonly id: number property after compilation to JavaScript?
In the function type (msg: string) => ____, the blank means "whatever this callback returns is ignored". Which type goes there?
What is the real difference between f(t?: string) and g(t: string | undefined)?
[1, 2, 3].map((n) => n * 2) compiles, even though map calls the callback with three arguments. Why?
interface Feeder { feed(a: Animal): void } accepts an object whose feed takes a Dog, even with strict on. What does that tell you?
Given function fmt(x: unknown): string; followed by function fmt(x: number): number; (plus an implementation), what is the type of fmt(1)?
Your team is arguing about interface vs type. Which claim holds?
- Parameter types are usually required, return types are usually inferred. Write the return type at a public boundary, and whenever you want a mistake reported at the definition instead of at some distant call site. It also stops
anyfrom leaking out of the function. ?lets the caller leave an argument out, and the type inside the function becomes| undefined. A default value also lets the caller leave it out, but the type inside the function does not includeundefined. Optional parameters must come last. A rest parameter is an array, or a tuple.voidin a function type means the caller ignores the return value, so a function that returns something still fits.voidon a declaration forbids returning a value. A variable of typevoidaccepts onlyundefined.- Return types are checked covariantly: returning
Dogfits whereAnimalis expected. Parameters are checked contravariantly understrictFunctionTypes— except for members written with method syntax, which stay bivariant. That exception is a deliberate unsoundness, and it is whyArray<Dog>is assignable toArray<Animal>. - A function with fewer parameters fits where one with more is expected, because JavaScript ignores extra arguments. That is why
array.map(x => x)compiles. Declaring more parameters than the target provides is rejected. - With overloads, the implementation signature cannot be called from outside, and TypeScript takes the first matching overload in source order rather than the best one. A
thisparameter is erased at compile time; arrow functions cannot have one. - Object types take three modifiers:
?for a field that may be missing,readonlyfor a compile-time-only write check, and[key: string]: Tfor keys that are only known at runtime. interfaceandtypeare interchangeable most of the time. Unions and mapped types needtype; declaration merging needsinterface.A & Bis an intersection: one value that satisfies both at once.