AniUI Academy

Basic Types and Inference

The primitive types, how TypeScript infers a type from a value without being told, and why annotating everything is usually the wrong instinct.

9 min read

TypeScript's most-used feature isn't the annotations you write — it's the ones you don't have to, because the compiler already worked out the type from the value itself. This lesson covers the primitive types and how inference decides what to assume.

The primitives

The types map directly onto JavaScript's own primitive values:

let isPublished: boolean = true;
let title: string = "Basic Types";
let minutes: number = 9;
let nothingYet: null = null;
let notSetYet: undefined = undefined;

There's no separate int or floatnumber covers every numeric value, matching how JavaScript itself only has one number type. string covers both single- and double-quoted strings and template literals.

Inference: the annotation you don't write

TypeScript reads the initial value of a variable and infers its type — you rarely need to write : string explicitly:

let title = "Basic Types"; // inferred as string, no annotation needed
let minutes = 9;           // inferred as number
 
title = 42;
// Type 'number' is not assignable to type 'string'.

Once inferred, the type sticks — reassigning title to a number is an error, exactly as if you'd written : string yourself. Inference isn't a weaker version of typing; it's the same checking, just without you having to spell out something the compiler could already see.

let widens, const doesn't

This distinction trips people up the first time they see it:

let a = 5;   // inferred as number — could be reassigned to any number
const b = 5; // inferred as 5 — a literal type, since it can never change

Because a can be reassigned, TypeScript widens the specific value 5 to the general type number — otherwise a = 10 would be an error, which would be useless. Because b is a const and can never change, TypeScript keeps the most precise type it can: the literal type 5. b is not just "a number" — it is, provably, always 5.

const b = 5;
let x: number = b; // fine — 5 is a number
let y: 10 = b;
// Type '5' is not assignable to type '10'.

Literal types like this become genuinely useful once you start writing unions ("pending" | "done" | "failed") and as const, both covered later in this track.

Arrays and tuples, briefly

An array of a single element type is written with [] or the generic Array<T> form — both mean exactly the same thing:

let scores: number[] = [10, 20, 30];
let names: Array<string> = ["Priya", "Jae"];

A different-looking but easy to confuse syntax, [string], is a tuple — a fixed-length array where each position has its own type. Tuples get a full lesson of their own shortly; for now, just don't reach for square brackets around a type expecting an array.

null, undefined, and strictNullChecks

Under strict mode (which this track assumes, and which every real project should turn on — covered in depth later), null and undefined are their own distinct types and are not silently part of every other type:

function getLength(text: string): number {
  return text.length;
}
 
let maybeText: string | undefined = undefined;
getLength(maybeText);
// Argument of type 'string | undefined' is not assignable
// to parameter of type 'string'.

Without strictNullChecks, that call would silently compile and crash at runtime the moment .length runs on undefined — exactly the bug type checking exists to prevent. Handling this properly (unions, narrowing, optional chaining) is the subject of several lessons still to come; for now, the important fact is that string alone means "definitely a string," never "a string, or maybe nothing."

When to annotate anyway

If inference already works, why annotate at all? A few real cases:

  • Function parameters. TypeScript cannot infer a parameter's type from nothing — function greet(name) leaves name implicitly any without an annotation (or an error, under noImplicitAny).
  • Widening you don't want reversed later. let status = "pending"; infers string, not the literal "pending" — if you later need "pending" | "done", an explicit annotation up front avoids surprises.
  • Documenting intent for a reader, even where inference would get it right anyway — particularly on exported functions, where the signature is effectively public API.

Try it yourself

Hover-equivalent: change let to const on the second line and see how the inferred type — and therefore what's allowed — changes.

Try it yourself
Loading playground...

What to remember

  • TypeScript infers a type from a value's initializer — annotate only where inference can't or where being explicit matters.
  • let widens a literal value to its general type; const keeps the narrowest possible literal type, since it can never change.
  • string[] and Array<string> are the same array type; [string] is a different thing — a tuple.
  • Under strict mode, null and undefined are distinct types that must be explicitly included in a union to be allowed.

Check yourself

4 questions · pass 3/4 to unlock Functions in TypeScript

up to 50
  1. 1.Given let count = 5; with no annotation, what type does TypeScript infer for count?

  2. 2.What type does const count = 5; infer, and why does it differ from let?

  3. 3.Which of these is the correct way to type an array of strings?

  4. 4.What is the practical difference between null and undefined in TypeScript's type system?

4 left to answer