TypeScriptPrimitives (string, number, boolean)

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.

TS
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.

TS
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         // number
Note
Always use the lowercase string 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:

TS
// 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'.
Warning
Never use 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:

TS
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.

TS
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:

TS
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)
Note
The 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.

TS
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.

TS
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:

TS
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:

TS
// 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:

TS
// 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'.
Tip
Use 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:

TS
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 template

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

Success
You now understand TypeScript's three core primitive types deeply. The key rules: always use lowercase type names, always use === 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.