The First Types to Learn in TypeScript
Before generics, utility types, or anything clever, TypeScript is mostly this: writing down that a value is a string, a number, an array, or an object with named fields. This guide covers those first annotations, plus the inference rules that decide when you do not need to write them at all.
The goal is reading ordinary TypeScript without stopping every three lines to decode notation. Once these basics are automatic, the rest of the types topic is far easier to follow.
The Primitive Annotations
An annotation is a colon after a name, followed by a type. The three primitives you will write constantly are string, number, and boolean, always lowercase:
let title: string = "Draft post";
let wordCount: number = 1240;
let published: boolean = false;
wordCount = "a lot"; // tsc output:
// error TS2322: Type 'string' is not assignable to type 'number'. That error is the whole product in miniature. You claimed wordCount holds numbers, the code broke the claim, and the compiler pointed at the exact line before you ran anything. By default tsc still emits JavaScript despite errors, so blocking a broken build needs noEmitOnError.
JavaScript has two empty values, and TypeScript types both: null and undefined. Under strictNullChecks, which is on in strict mode, neither sneaks into a string or number without being declared. A value that might be absent gets a union, such as string | null, and the compiler then requires a narrowing check before operations that need the string itself, like calling a string method, though passing the union along needs none. The unions and narrowing guide covers those checks in depth.
Arrays
An array type is the element type followed by square brackets. Every element must match it, and methods like push are checked against it too:
const tags: string[] = ["typescript", "types"];
tags.push("beginners");
tags.push(42); // tsc output:
// error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'. The equivalent generic form Array<string> means exactly the same thing. Use whichever reads better; string[] wins for short element types, and the generic form wins when the element type is itself a mouthful.
One sharp edge worth knowing early: indexing can miss. tags[10] is typed string by default even though the array has three elements. The compiler flag noUncheckedIndexedAccess changes that to string | undefined, which is more honest and more work. Teams that handle a lot of lookups tend to turn it on.
Object Types
Object types list each property with its own type. This is where annotations start paying rent, because object shapes are the thing teams actually disagree about:
type Article = {
title: string;
wordCount: number;
tags: string[];
subtitle?: string;
reviewedBy: string | null;
};
const post: Article = {
title: "First types",
wordCount: 980,
tags: ["typescript"],
reviewedBy: null,
};
console.log(post.subtitle);
// prints: undefined Two markers here do a lot of work. The ? on subtitle makes the property optional, so the object literal may omit it entirely. The string | null on reviewedBy means the property must always be present, but its value may be null.
Strictly, ? permits omission and | null permits a null value, nothing more. Most teams add a convention on top: optional for "this field may not apply", nullable for "this field always applies, and it may be empty right now". Agreeing on that convention keeps every reader of the type honest about what absence means.
Extra properties get caught too, at least on fresh object literals. Assigning { title: "x", wordCount: 1, tags: [], reviewedBy: null, wordcount: 2 } fails with an excess-property error, which catches typos like the lowercase wordcount at the moment they are written. The check fires whenever a fresh literal is checked directly against a target object type, in typed assignments and function arguments alike, unless the target permits the key through an index signature; assigning it to an untyped variable first loses that freshness and falls back to structural assignability.
Type Inference
TypeScript infers types from values, so most local variables need no annotation at all. The interesting part is that for an unannotated primitive literal, let and const infer differently:
let status = "draft";
const mode = "draft";
// hover in an editor shows:
// let status: string
// const mode: "draft" let widens to string because the variable can be reassigned to any other string. const keeps the literal type "draft" because the binding can never change, though an annotation or as const can keep a let narrow, and object properties and array elements widen even inside a const because their contents stay mutable. That distinction, called widening, is why a const often slots neatly into a literal union parameter while the same value in a let gets rejected.
Return types are inferred too. Given name: string | null and email: string, a function returning user.name ?? user.email infers string without being told, and the inference updates itself as the body changes. Inference covers initialized variables, return values, parameters with defaults, and contextually typed callbacks; a standalone declaration's own parameters have none of that, which is why those carry annotations.
When to Annotate
The working rule: annotate intent, not trivia. An annotation earns its place when it records a decision the code alone does not state, and it becomes noise when it repeats what inference already knows:
// noise: inference already knows all of this
const retries: number = 3;
const labels: string[] = ["draft", "published"];
// intent: these record decisions
type ArticleStatus = "draft" | "published" | "archived";
export function canPublish(status: ArticleStatus): boolean {
return status === "draft";
}
console.log(canPublish("draft"));
// prints: true The ArticleStatus type is a business rule: only three values are legal. The parameter annotation enforces it at every call site the compiler checks, and the explicit boolean return locks the function's contract even if someone rewrites the body later. Both are compile-time guarantees, so they hold only where nothing escapes through any, assertions, or unvalidated runtime data.
In practice, most teams converge on the same short list. Annotate standalone function parameters, since inference has nothing to work with there, and leave contextually typed callback parameters alone. Annotate the returns of exported functions, so a refactor inside the body cannot silently change the public contract. Name shared data shapes as types, and let locals infer unless you want a target shape or an excess-property check.
Pitfalls and Debugging
Parameter 'x' implicitly has an 'any' type. The most common first error in strict mode. A standalone parameter with no default and no contextual type has nothing to infer from, so it falls back to any, and noImplicitAny rejects that. The fix is an annotation on that parameter, not on the call site; defaulted parameters and callbacks in typed positions infer without one.
Annotating too early and fighting the error in the wrong place. When an assignment fails, the mismatch is sometimes in the annotation rather than the value. Read the error's expected and actual types before changing code, then correct whichever side breaks the contract you actually intended: sometimes that is widening the annotation, sometimes it is reshaping the data.
Using the wrapper types. String, Number, and Boolean with capitals are object wrapper types from JavaScript. They accept primitives but not the other way round, producing baffling errors far from the cause. Write the lowercase primitives everywhere; the capitalized forms are almost never intentional outside of rare interop code.
Expecting the type to exist at runtime. typeof post in running code returns "object", not Article, because types are erased at compilation. Checking which shape a runtime value has is a narrowing problem, covered in the narrowing guide and the runtime data guide.
Frequently Asked Questions
Why does TypeScript say a variable is implicitly any?
The compiler could not infer a type and strict mode refuses to guess. It usually means a standalone function parameter with no annotation, no default, and no contextual type to infer from. Add the annotation, or accept unchecked code by disabling noImplicitAny, which most teams avoid.
Should you use Number or number as a type?
Use lowercase number, string, and boolean. The capitalized versions name JavaScript wrapper object types, which are almost never what you mean: primitives are assignable to them, but actual boxed instances behave like objects. TypeScript accepts both, but the lowercase primitives are the ones the standard library expects.
What's the difference between string[] and Array<string>?
Nothing at the type level: they describe the same type and you can mix them freely. string[] is shorter and more common for simple element types. Array<string> reads better when the element type is itself long, such as an object type or a union.
Does an optional property mean the same as string or undefined?
Close, but not identical: an optional property may be missing entirely, while a string | undefined property must always be present, even if its value is undefined. Without exactOptionalPropertyTypes, the optional form also accepts an explicit undefined; with it enabled, writing undefined requires adding | undefined. Reading an optional property still gives string | undefined.
Related
- TypeScript Types for the full topic overview
- Interfaces and Type Aliases for the two ways to name object shapes
- Union Types and Narrowing for what
string | nullreally unlocks - TypeScript for the language guide, including tsconfig and tooling
Sources
-
[1]
Everyday Types(typescriptlang.org)
-
[2]
Type Inference(typescriptlang.org)
-
[3]
Object Types(typescriptlang.org)
Read Next
Interfaces and type aliases both name shapes in TypeScript. Learn the real differences: extension, declaration merging, what each form can express, and which to default to.
Union types, literal types, and the narrowing toolkit: typeof, in, instanceof, discriminated unions, and exhaustiveness checking with never.
The TypeScript type system from the ground up: the first annotations, interfaces and type aliases, union types and narrowing, and handling unknown runtime data.