Primitive Types: string, number, boolean
TypeScript builds on JavaScript's three most fundamental value types — string,
number, and boolean. These are called primitives: they hold a single
immutable value and are compared by value, not by reference. Understanding
exactly how TypeScript models them — and the traps they hide — is the foundation
for everything else in the type system.
The string Type
In TypeScript, all text is typed as string. It doesn't matter whether the
value is a single character, a sentence, or a multi-line template literal —
TypeScript uses one type for all of it.
let firstName: string = 'Alice' let greeting: string = "Hello, world" let multiLine: string = `Line one Line two` // TypeScript infers string from the literal — annotation is optional let city = 'Toronto' // city: string
Template literals are first-class strings. TypeScript tracks the interpolated
expressions and ensures they produce a value. Any primitive — number, boolean —
is allowed inside a template expression because TypeScript calls .toString()
implicitly.
const count: number = 5
const isNew: boolean = true
const message: string = `You have ${count} new messages (${isNew ? 'new' : 'old'})`
// TypeScript knows .toUpperCase() exists on string
const shout = message.toUpperCase() // string
const length = message.length // numberstring type annotation, never the uppercase String. The uppercase form refers to the JavaScript wrapper object, which behaves differently and causes subtle bugs. TypeScript will warn you if you use String as a type annotation in most contexts.string vs String — Why It Matters
JavaScript has two string forms: the primitive "hello" and the boxed object
new String("hello"). They are not the same:
// Primitive
const a = 'hello'
typeof a // "string"
// Boxed object (almost never what you want)
const b = new String('hello')
typeof b // "object"
a === b // false — different types!
// TypeScript correctly flags this assignment:
const name: string = new String('Alice')
// ~~~~~~~~~~~~~~~~~~
// Type 'String' is not assignable to type 'string'.new String(), new Number(), or new Boolean(). Always use primitive literals. The uppercase type annotations exist in TypeScript only to describe those rare wrapper objects — you will almost never need them.Common String Methods TypeScript Knows About
Because TypeScript knows a value is a string, it gives you accurate
autocomplete and return-type information for every built-in string method:
const email = 'user@example.com'
email.includes('@') // boolean
email.split('@') // string[]
email.indexOf('.') // number
email.slice(0, 4) // string
email.replace('@', ' at ') // string
email.trim() // string
email.padStart(20, '.') // string
email.startsWith('user') // boolean
// TypeScript catches mistakes at compile time
email.includes(42)
// ~~
// Argument of type 'number' is not assignable to parameter of type 'string'.The number Type
TypeScript (and JavaScript) use a single number type for all numeric values —
integers, floats, negatives, very large values, and the special numeric sentinels
NaN and Infinity. There is no separate int or float type.
let age: number = 30 let price: number = 9.99 let negative: number = -273.15 let hex: number = 0xFF // 255 let binary: number = 0b1010 // 10 let octal: number = 0o17 // 15 let big: number = 1_000_000 // underscores for readability (ES2021+) // Special values — still typed as number const notANumber: number = NaN const posInfinity: number = Infinity const negInfinity: number = -Infinity
TypeScript knows the return types of all arithmetic and Math operations:
const x = 10 const y = 3 x + y // number x / y // number (3.333...) x % y // number (remainder: 1) Math.floor(x / y) // number (3) Math.pow(x, 2) // number (100) Math.sqrt(x) // number (3.162...) Math.abs(-x) // number (10)
NaN !== NaN quirk is inherited from IEEE 754 floating point. To check for NaN, always use Number.isNaN(value) — never value === NaN, which is always false.const bad = NaN // Wrong — always false, even for NaN console.log(bad === NaN) // false // Correct console.log(Number.isNaN(bad)) // true console.log(isNaN(bad)) // true (less precise — coerces first)
The boolean Type
The boolean type has exactly two values: true and false. TypeScript
infers boolean from literal assignments and tracks it through logical
operations and comparisons.
let isActive: boolean = true let hasPermission: boolean = false // TypeScript infers boolean const isLoggedIn = false // boolean // Comparison operators always produce boolean const isAdult = age >= 18 // boolean const isEmpty = arr.length === 0 // boolean const matches = name === 'Alice' // boolean // Logical operators preserve boolean const canAccess = isLoggedIn && hasPermission // boolean const showBanner = !isLoggedIn || !hasPermission // boolean
Type Narrowing with Primitives
When TypeScript doesn't know which primitive a variable holds (for example,
in a union type), you can use typeof to narrow the type. Inside each
narrowed branch TypeScript knows the exact type and provides the correct methods:
function formatValue(value: string | number | boolean): string {
if (typeof value === 'string') {
// TypeScript knows: value is string here
return value.toUpperCase()
}
if (typeof value === 'number') {
// TypeScript knows: value is number here
return value.toFixed(2)
}
// TypeScript knows: value is boolean here
return value ? 'Yes' : 'No'
}
console.log(formatValue('hello')) // "HELLO"
console.log(formatValue(3.14159)) // "3.14"
console.log(formatValue(true)) // "Yes"Equality: === vs ==
TypeScript strongly encourages === (strict equality) over == (loose
equality). Loose equality performs type coercion before comparing, which produces
surprising results. TypeScript can even flag comparisons that are statically
always false:
// Loose equality — coerces types (avoid these)
console.log(0 == false) // true (!)
console.log('' == false) // true (!)
console.log(null == undefined) // true (!)
console.log('5' == 5) // true (!)
// Strict equality — no coercion
console.log(0 === false) // false
console.log('' === false) // false
console.log(null === undefined) // false
console.log('5' === 5) // false
// TypeScript statically detects this is always false:
const num: number = 5
if (num === 'hello') { }
// ~~~~~~~~~~~~~~~~~
// This condition will always return 'false' since the types 'number'
// and 'string' have no overlap.Implicit toString() Coercion Pitfalls
JavaScript's + operator is overloaded: when either operand is a string, it
concatenates instead of adding. This creates a classic family of bugs that
TypeScript catches when your types are explicit:
// Bug: value is a string coming from an <input> field
const rawInput: string = '5'
// This is string concatenation, not addition!
const result = rawInput + 10 // "510", not 15
// Fix: convert explicitly before arithmetic
const correct = Number(rawInput) + 10 // 15
const alsoCorrect = parseInt(rawInput, 10) + 10 // 15
// TypeScript can warn you when strictness is turned on:
function add(a: number, b: number): number {
return a + b
}
add(rawInput, 10)
// ~~~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'.Number(x) to convert a string to a number. It returns NaN if the string is not a valid number, which you can then catch with Number.isNaN(). Use parseInt(x, 10) when you only want the integer portion.Real-World Example: Form Validation
Here is a practical form validation function that uses all three primitive types. TypeScript ensures every branch returns the correct type and catches any mismatched assignments at compile time:
interface ValidationResult {
valid: boolean
message: string
fieldName: string
}
function validateField(
fieldName: string,
value: string,
minLength: number,
required: boolean
): ValidationResult {
// required is boolean — used directly in the condition
if (required && value.trim().length === 0) {
return {
valid: false,
message: `${fieldName} is required`,
fieldName,
}
}
// minLength is number — arithmetic is type-safe
if (value.length < minLength) {
return {
valid: false,
message: `${fieldName} must be at least ${minLength} characters`,
fieldName,
}
}
return {
valid: true,
message: `${fieldName} looks good!`,
fieldName,
}
}
const r1 = validateField('Username', '', 3, true)
console.log(r1.valid, r1.message)
const r2 = validateField('Username', 'Al', 3, true)
console.log(r2.valid, r2.message)
const r3 = validateField('Username', 'Alice', 3, true)
console.log(r3.valid, r3.message)Quick Reference
Concept | Correct | Avoid |
|---|---|---|
Text type annotation | string | String |
Numeric type annotation | number | Number |
Boolean type annotation | boolean | Boolean |
Create a string | 'hello' or | new String("hello") |
Check for NaN | Number.isNaN(x) | x === NaN |
Equality check | x === y | x == y |
String to number | Number(x) or parseInt(x, 10) | +'5' or x * 1 |
=== for equality, use Number.isNaN() for NaN checks, and rely on typeof narrowing in union scenarios. These habits will save you from an entire class of runtime bugs.