Basic Types & Inference

TypeScript · Lesson 4 of 28 7 min read Updated

Every value in a JavaScript program has a type: "Kochi" is a string, 42 is a number, true is a boolean. TypeScript lets you write these types down, and more importantly, it works most of them out for you.

In this lesson you will learn the basic (primitive) types, how to write a type annotation, and how type inference decides types when you do not write them. Knowing when to annotate and when to let TypeScript infer is one of the most useful habits you can build.

Type annotations

A type annotation is a colon followed by a type, written after a variable name or parameter:

TypeScriptRun it yourself
let studentName: string = "Ananya";
let rollNumber: number = 27;
let hasPaidFee: boolean = true;

console.log(studentName, rollNumber, hasPaidFee);
console.log(typeof studentName, typeof rollNumber, typeof hasPaidFee);
Output
Ananya 27 true
string number boolean

The annotation says "this variable may only hold this kind of value". Once rollNumber is a number, you cannot put text into it later. Notice that the JavaScript typeof operator agrees with the TypeScript types for these simple values, because TypeScript's primitive types are named after JavaScript's run-time types.

The primitive types

TypeExample valuesNotes
string"Pune", 'हिंदी', `Hi ${name}`Any text
number42, 3.14, -7, 1_299Whole numbers and decimals share one type
booleantrue, falseOnly these two values
bigint9007199254740993nWhole numbers of any size, written with n
symbolSymbol("id")Unique keys, rarely needed
nullnull"Deliberately empty"
undefinedundefined"Not set yet"

There is no separate int or float type: number covers both, exactly like JavaScript. Use bigint only when whole numbers go beyond about 9 quadrillion (Number.MAX_SAFE_INTEGER), for example in some ID systems or cryptography.

TypeScriptRun it yourself
const safeLimit: number = Number.MAX_SAFE_INTEGER;
const tooBig: number = safeLimit + 2;
const exact: bigint = BigInt(safeLimit) + 2n;

console.log(safeLimit);
console.log(tooBig);
console.log(exact);
Output
9007199254740991
9007199254740992
9007199254740993n

The number result is wrong by one because it is past the safe limit. The bigint result is exact. You cannot mix the two in one calculation without converting, which TypeScript also checks for you.

What type errors look like

When a value does not match its type, tsc reports it. Here are three common mistakes:

TypeScript
let city: string = "Lucknow";
city = 226001;

let price: number = 499;
price = "499";

let greeting = "namaste";
console.log(greeting.toUppercase());
Output
app.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
app.ts(5,1): error TS2322: Type 'string' is not assignable to type 'number'.
app.ts(8,22): error TS2551: Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?

Read error messages slowly. "Type X is not assignable to type Y" means "you tried to put an X where a Y is required". The third error even suggests the correct method name. The variable greeting has no annotation at all, yet TypeScript still knew it was a string. That is inference.

Type inference

Type inference means TypeScript works out a type from the value you give. When you write let marks = 88, TypeScript infers number, exactly as if you had written let marks: number = 88. In VS Code, hover over a variable to see the inferred type.

TypeScriptRun it yourself
let marks = 88;          // inferred: number
let subject = "Physics"; // inferred: string
let passed = marks >= 33; // inferred: boolean

const fees = [1200, 1500, 900]; // inferred: number[]
const total = fees.reduce((sum, fee) => sum + fee, 0); // inferred: number

console.log(subject, marks, passed);
console.log("Total fees:", total);
Output
Physics 88 true
Total fees: 3600

Inference works through expressions too: TypeScript knows that marks >= 33 gives a boolean and that adding numbers gives a number. So when should you write types yourself?

  • Let TypeScript infer local variables that get a value immediately. Extra annotations add noise and can go out of date.
  • Annotate function parameters. TypeScript cannot guess what callers will pass. You will see this in Functions.
  • Annotate variables declared without a value, such as let total: number;, because there is nothing to infer from.
  • Annotate when you want a wider or stricter type than the value suggests, for example a variable that starts as null but will later hold a string.

let versus const

let and const are inferred slightly differently. let city = "Kochi" is inferred as string, because you might change it to another city. const city = "Kochi" can never change, so TypeScript infers the exact type "Kochi", called a literal type. You will use this in Literal Types, as const & satisfies.

null and undefined

With strict mode (on by default in TypeScript 7), null and undefined are separate types. A string variable cannot hold null, and TypeScript makes sure you never read a variable before giving it a value:

TypeScript
let nickname: string = null;

let discount: number;
console.log(discount * 2);
Output
app.ts(1,5): error TS2322: Type 'null' is not assignable to type 'string'.
app.ts(4,13): error TS2454: Variable 'discount' is used before being assigned.

This check, called strict null checking, removes a whole family of bugs: "Cannot read properties of null" is one of the most common JavaScript crashes. When a value really may be missing, say so with a union type: let nickname: string | null = null;. The | means "or". You will learn unions properly in Unions & Narrowing.

Common mistakes

  • Annotating everything. const total: number = 5 + 7; works, but the annotation adds nothing. Let inference do its job for simple local values.
  • Using String instead of string. The capital letter version is a different, rarely wanted type.
  • Expecting int or float. TypeScript has one number type for both.
  • Forgetting that types vanish at run time. let age: number = Number(input) can still be NaN if the input is not a number. Check outside data with real code.

Key points

  • Write a type annotation as name: type, for example let marks: number = 88.
  • The primitive types are string, number, boolean, bigint, symbol, null and undefined, all lowercase.
  • Type inference works out types from values, so local variables with a value rarely need annotations.
  • Annotate function parameters and variables that start without a value.
  • In strict mode, null and undefined must be allowed explicitly, for example with string | null.

Exercise

A cricket scorer in Rohtak tracks one innings. Declare variables for the team name, runs scored, wickets lost, whether the innings is complete, and the overs bowled (for example 19.4). Use inference where it is enough and an annotation where it is needed. Then declare let target: number;, give it the value runs + 1 and log a sentence such as Rohtak Rhinos: 186/7 in 20 overs. Target: 187.

Show solution
TypeScript
const team = "Rohtak Rhinos";
let runs = 186;
let wickets = 7;
let overs = 20;
let isComplete = true;

let target: number;
target = runs + 1;

console.log(`${team}: ${runs}/${wickets} in ${overs} overs. Target: ${target}`);
console.log("Innings complete:", isComplete);

Every variable with a starting value is inferred. Only target needs an annotation, because it is declared before it has a value.

Quick Quiz

1. What type does TypeScript infer for let fee = 1299;?

2. Which annotation is correct for a name?

3. With strict mode on, what does let city: string = null; give?

4. When is an annotation most useful?

Learn TypeScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call