When you first meet TypeScript, the thing that jumps out is a small colon followed by a word: name: string. That tiny piece of syntax is called a type annotation, and it is how you tell TypeScript what kind of value something is allowed to hold. Once you understand annotations, a huge part of TypeScript suddenly clicks into place, because nearly everything else builds on top of them.
In this article we will go slowly. You will see how to annotate a variable, a function parameter, and a function’s return value. We will also look at the flip side: the moments when you do not need to write any annotation at all, because TypeScript can work the type out on its own. Knowing the difference is what separates code that is cluttered with noise from code that is clean and still fully safe.
What a type annotation actually is
A type annotation is a note you attach to a name, telling TypeScript: “this thing will only ever be a value of this type.” The syntax is always the same shape. You write the name, then a colon, then the type.
let username: string = "Jane Doe";
let age: number = 27;
let isActive: boolean = true;
Read each line out loud and it almost sounds like English. “Let username be a string, set to Jane Doe.” The part after the colon — string, number, boolean — is the type. The part after the equals sign is the actual value.
Here is the important idea: the annotation is a promise, and TypeScript holds you to it. If you promise age will be a number, you cannot later sneak a piece of text into it.
let age: number = 27;
age = 30; // fine, still a number
age = "thirty"; // Error: Type 'string' is not assignable to type 'number'.
That error appears in your editor, before you ever run the program. This is the whole point of TypeScript: it catches the mistake while you are typing, not later when a user is clicking around your app.
Annotations disappear at runtime
Type annotations are only there to help you and your editor during development. When your code is compiled to plain JavaScript, every annotation is stripped away. The browser never sees : string. Think of annotations as instructions for a careful reviewer who reads your code before it ships.
The basic building-block types
Before going further, it helps to know the handful of basic types you will reach for most often. These are the primitives — the simplest values a program works with.
| Type | What it holds | Example value |
|---|---|---|
string |
text | "hello", "Jane Doe" |
number |
any number, whole or decimal | 42, 3.14, -7 |
boolean |
a true/false flag | true, false |
null |
the deliberate absence of a value | null |
undefined |
a value that was never set | undefined |
You will meet richer types later — arrays, objects, and custom shapes you define yourself — but everything starts from these. An array of names, for example, is annotated as string[], which reads as “an array of strings.”
let tags: string[] = ["sale", "new", "featured"];
let scores: number[] = [98, 76, 84];
Annotating function parameters
Variables are a gentle place to start, but annotations really earn their keep on functions. A function takes inputs (called parameters) and usually gives back an output. Annotating the parameters tells everyone — including future you — exactly what the function expects to be handed.
function greet(name: string) {
return "Hello, " + name + "!";
}
greet("Jane Doe"); // "Hello, Jane Doe!"
greet(42); // Error: number is not assignable to string
Each parameter gets its own annotation, separated by commas, just like you would expect.
function createUser(name: string, age: number, isAdmin: boolean) {
return { name, age, isAdmin };
}
Now anyone calling createUser is guided. If they forget an argument, pass them in the wrong order, or hand over a value of the wrong type, TypeScript stops them immediately. The function’s signature becomes a small contract that the rest of the code has to honor.
Parameters are where annotations matter most
If you only annotate one thing in your code, make it your function parameters. TypeScript usually cannot guess what a parameter should be — it has no value to look at yet — so without an annotation it falls back to a loose “anything goes” type, which quietly turns off most of the safety you came for.
Annotating the return value
A function also produces something on the way out, and you can annotate that too. The return type goes after the parameter list, separated by a colon, right before the function body’s opening brace.
function add(a: number, b: number): number {
return a + b;
}
That trailing : number says “this function promises to give back a number.” It is a promise TypeScript enforces on the inside of the function as well. If your code accidentally returns the wrong kind of thing, you get told right where the mistake is.
function getName(): string {
return 42; // Error: Type 'number' is not assignable to type 'string'.
}
There is also a special return type for functions that do not return anything useful — they just perform an action. That type is void.
function logMessage(message: string): void {
console.log(message);
// no return statement, and that is exactly right
}
void is your way of saying “this function runs for its effect, not for a value it hands back.”
When you do NOT need an annotation
Here is where many beginners over-do it. TypeScript is smart. In a lot of situations it can look at your code and figure out the type by itself. This is called type inference, and leaning on it keeps your code tidy.
The clearest example is a variable that you create and assign in one go:
let city = "Jakarta"; // TypeScript infers: string
let count = 10; // TypeScript infers: number
let ready = false; // TypeScript infers: boolean
You did not write : string, yet TypeScript knows city is a string, because you handed it the value "Jakarta" on the same line. Try to break the promise and it still stops you:
let city = "Jakarta";
city = 99; // Error: Type 'number' is not assignable to type 'string'.
So the annotation here would be redundant — extra typing that buys you nothing. The same goes for return values. TypeScript can usually infer what a function returns by looking at the return statement.
function add(a: number, b: number) {
return a + b; // inferred return type: number
}
Notice we still annotated the parameters, because TypeScript cannot guess those. But the return type was left to inference, and that is perfectly clean code.
Has a value on the same line? ──► Inference can do it ──► annotation optional
No value yet (a parameter)? ──► Inference is stuck ──► annotation needed
So the rough rule is simple: annotate where TypeScript is blind, let inference handle where it can see. Function parameters are blind. Variables you assign immediately, and most return values, are seen.
One annotation you really do want
The big exception is a variable you declare now but assign later. With no value to look at, TypeScript infers the type any, which switches off checking for that variable. If you write let result; and only assign to it further down, give it an explicit annotation like let result: number; so it stays protected.
There is a deeper story to inference — how TypeScript narrows types, how it handles trickier cases, and when it gives up. That is a topic of its own, and a good next step once annotations feel natural.
A small, complete example
Let us tie it all together with a function that builds a short profile string for a user. Watch which parts are annotated and which are left to inference.
function describeUser(name: string, age: number, isAdmin: boolean): string {
const role = isAdmin ? "administrator" : "member"; // inferred: string
const summary = name + " (" + age + ", " + role + ")"; // inferred: string
return summary;
}
const text = describeUser("John Doe", 34, false);
// text is inferred as: string
console.log(text); // "John Doe (34, member)"
Look at the balance. The parameters and the return type carry explicit annotations, because they form the function’s public contract — the thing other code relies on. Inside the body, role and summary get their types inferred, because TypeScript can see the values right there. And text, the result of calling the function, needs no annotation at all, because the function already promised to return a string.
That is idiomatic TypeScript: explicit at the boundaries, relaxed in the middle.
Recap
Type annotations are the : Type notes you attach to names so TypeScript knows what each value is allowed to be. Here is what to carry forward:
- The syntax is always
name: Type, whether for a variable, a parameter, or a return value. - An annotation is a promise TypeScript enforces, catching mismatched types as you write rather than when the app runs.
- Always annotate function parameters — TypeScript has no value to infer from, so skipping this quietly removes your safety net.
- Annotate return types when you want an explicit contract, but it is fine to let inference handle them too.
- Skip annotations on variables you assign immediately; inference already knows the type, and an annotation would just be noise.
- Watch out for variables declared without a value — those are worth annotating so they do not silently become
any.
Master this small piece of syntax and you have the foundation for everything else in TypeScript. The natural next stop is type inference itself: understanding exactly how TypeScript figures things out so you know precisely when you can lean on it.