TypeScript Basic Types: Annotations and Inference
Start with TypeScript annotations for strings, numbers, booleans, arrays, and objects. These describe the values your code expects. TypeScript can also infer many types from assignments, so you do not need to label every variable.
Learn these annotations first so you can read a function or object without stopping to decode each type. The rest of the types topic builds on them.
How Do You Write TypeScript Type Annotations?
Write a TypeScript type annotation as a colon after a variable or parameter name, followed by its 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'. The error tells you that wordCount expects a number, but the assignment supplies a string. You can fix the mismatch before running the code. By default tsc still emits JavaScript despite errors, so blocking a broken build needs noEmitOnError.
Use null or undefined for values that may be missing. With strictNullChecks enabled, a nullable string needs a type such as string | null. Check that the value is a string before calling its methods.
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.
Indexing can miss, which is worth knowing early. tags[10] is typed string by default even though the array has three elements. The compiler flag noUncheckedIndexedAccess changes that to string | undefined, so you must handle a missing element before using it.
Object Types
TypeScript object types list the properties an object needs and the type of each property. A type alias or interface gives that shape a reusable name:
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.
Choose optional properties when a field can be omitted. Use a required nullable property when the field must be present but can have no value. Keep that distinction consistent in your application.
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 applies to fresh object literals in typed assignments and function arguments. A type with an index signature can allow extra keys; an object already stored in a variable is checked differently.
How Does TypeScript Infer Types?
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 infers string because the variable can hold another string later. const keeps the exact type "draft". This difference is called widening.
Object properties and array elements can still widen inside a const, because their contents can change. Add an annotation or use as const when you need to preserve specific allowed values.
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 Should You Add TypeScript Annotations?
Add TypeScript annotations to parameters the compiler cannot infer and to values that need a specific allowed type. Let inference handle straightforward local variables:
// 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?
TypeScript reports implicit any when it cannot infer a type and noImplicitAny is enabled, either directly or through strict. A common cause is a standalone function parameter without an annotation, default value, or contextual type. Add the expected parameter type rather than turning off the check.
Should you use Number or number as a TypeScript type?
Use lowercase number for numeric values in TypeScript, and use string and boolean for the other primitives. 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> in TypeScript?
TypeScript string[] and Array<string> describe the same array type, so you can use either form. 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.
Is an optional TypeScript property the same as string | undefined?
An optional TypeScript property can be absent, while a required property typed string | undefined must be present even when 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.
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.