Learning Guides
Menu

Understanding TypeScript

7 min readEffective TypeScript

Understanding TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. Before diving into specific techniques, it's essential to understand what TypeScript is, how it relates to JavaScript, and what problems it solves.

The Relationship Between TypeScript and JavaScript

Note

TypeScript is a superset of JavaScript. This means all valid JavaScript is also valid TypeScript, but TypeScript adds optional static type checking and additional language features.

Item 1: Understand the Relationship Between TypeScript and JavaScript

The most fundamental thing to understand about TypeScript is its relationship with JavaScript:

TYPESCRIPT
// This is valid JavaScript AND valid TypeScript
function greet(name) {
  return "Hello, " + name;
}
 
// This is valid TypeScript but NOT valid JavaScript
function greetTyped(name: string): string {
  return "Hello, " + name;
}

TypeScript's type system is designed to model JavaScript's runtime behavior. This has important implications:

TYPESCRIPT
// JavaScript allows this at runtime
const x = 2 + "3"; // '23' - number coerced to string
 
// TypeScript models this behavior
const y: string = 2 + "3"; // TypeScript knows this is a string

However, TypeScript also catches many common errors that JavaScript would silently accept:

TYPESCRIPT
const a = null + 7; // TypeScript error, JavaScript returns 7
const b = [] + 12; // TypeScript error, JavaScript returns '12'

The Type System Goals

TypeScript's type system has several key goals:

  1. Catch errors at compile time rather than runtime
  2. Provide better tooling (autocomplete, refactoring, navigation)
  3. Serve as documentation that stays in sync with the code
  4. Model JavaScript's runtime behavior accurately

Catching Errors Early

TYPESCRIPT
interface User {
  name: string;
  email: string;
}
 
function sendEmail(user: User) {
// TypeScript provides autocomplete for user.name, user.email
console.log(`Sending email to ${user.email}`);
}
 
// Error caught at compile time!
sendEmail({ name: 'Alice' });
// Property 'email' is missing in type '{ name: string; }'
 

Item 2: Know Which TypeScript Options You're Using

TypeScript's behavior is highly configurable through tsconfig.json. The most important options relate to strictness:

JSON
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

The Most Important Strict Options

Warning

If you're starting a new project, always enable strict: true. It's much harder to enable strictness later in an existing codebase.

noImplicitAny: Disallows variables with implicit any type

TYPESCRIPT
// With noImplicitAny: false (dangerous!)
function add(a, b) {
  // a and b are 'any'
  return a + b;
}
add("hello", [1, 2, 3]); // No error, but probably a bug!
 
// With noImplicitAny: true (safe)
function addStrict(a: number, b: number) {
  return a + b;
}

strictNullChecks: Makes null and undefined distinct types

TYPESCRIPT
// With strictNullChecks: false
const x: string = null; // Allowed but dangerous!
 
// With strictNullChecks: true
const y: string = null; // Error!
const z: string | null = null; // Must be explicit

Item 3: Understand That Code Generation Is Independent of Types

Note

TypeScript's type system is completely erased at runtime. Types have zero runtime presence.

This has several important implications:

Types Cannot Affect Runtime Behavior

TYPESCRIPT
function asNumber(val: number | string): number {
  return val as number; // WRONG! This doesn't convert anything
}
 
asNumber("123"); // Returns '123', not 123!
 
// Correct approach: use runtime checks
function asNumberCorrect(val: number | string): number {
  return typeof val === "string" ? Number(val) : val;
}

You Cannot Check Types at Runtime

TYPESCRIPT
interface Square {
  width: number;
}
 
interface Rectangle {
  width: number;
  height: number;
}
 
type Shape = Square | Rectangle;
 
function calculateArea(shape: Shape) {
  // ERROR: 'Rectangle' only refers to a type
  if (shape instanceof Rectangle) {
    return shape.width * shape.height;
  }
  return shape.width * shape.width;
}

The solution is to use "tagged unions" or property checks:

TYPESCRIPT
interface Square {
  kind: "square";
  width: number;
}
 
interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
 
type Shape = Square | Rectangle;
 
function calculateArea(shape: Shape): number {
  // Now we can check at runtime!
  if (shape.kind === "rectangle") {
    return shape.width * shape.height;
  }
  return shape.width * shape.width;
}

Type Operations Cannot Affect Runtime Values

TYPESCRIPT
function add(a: number, b: number) {
  return a + b;
}
 
// The emitted JavaScript has no types:
// function add(a, b) {
//   return a + b;
// }

Item 4: Get Comfortable with Structural Typing

TypeScript uses structural typing (also called "duck typing"). This means type compatibility is determined by the structure of types, not their declared names.

Structural Typing in Action

TYPESCRIPT
interface Point2D {
  x: number;
  y: number;
}
 
interface Point3D {
x: number;
y: number;
z: number;
}
 
function logPoint(point: Point2D) {
console.log(`(${point.x}, ${point.y})`);
}
 
const point3d: Point3D = { x: 1, y: 2, z: 3 };
 
// This works! Point3D is structurally compatible with Point2D
logPoint(point3d);
 

This is powerful but can lead to surprises:

TYPESCRIPT
interface Vector2D {
  x: number;
  y: number;
}
 
function calculateLength(v: Vector2D) {
  return Math.sqrt(v.x * v.x + v.y * v.y);
}
 
// This object has extra properties
const namedVector = { x: 3, y: 4, name: 'my vector' };
 
// Still works due to structural typing
calculateLength(namedVector); // Returns 5

When Structural Typing Causes Problems

TYPESCRIPT
interface Author {
  name: string;
  books: string[];
}
 
interface Database {
  runQuery(query: string): unknown[];
}
 
function getAuthors(db: Database): Author[] {
  const rows = db.runQuery("SELECT name, books FROM authors");
  return rows as Author[]; // Dangerous!
}

Warning

Structural typing means TypeScript trusts that if an object has the right shape, it has the right type. This trust can be misplaced when dealing with external data.

Item 5: Limit Use of the any Type

The any type is TypeScript's escape hatch. It disables type checking for a value:

TYPESCRIPT
let age: any = "12";
age = 12; // No error
age = { years: 12 }; // No error
age.push(4); // No error at compile time, runtime crash!

Problems with any

  1. No type safety: Any operation is allowed
  2. Breaks contracts: Functions expecting specific types get garbage
  3. No autocomplete: Editor can't help you
  4. Hides refactoring bugs: Changes won't be flagged
  5. No documentation: Readers can't understand the code

The Dangers of any

TYPESCRIPT
function calculateAge(birthDate: Date): number {
  // ... implementation
  return 30;
}
 
let birthDate: any = '1990-01-01';
 
// No error, but this will fail or produce wrong results!
calculateAge(birthDate);
 

Better Alternatives to any

TYPESCRIPT
// 1. Use unknown instead of any for values of unknown type
function processInput(input: unknown) {
  // Must narrow before use
  if (typeof input === 'string') {
    return input.toUpperCase();
  }
  if (Array.isArray(input)) {
    return input.length;
  }
  throw new Error('Invalid input');
}
 
// 2. Use type parameters for flexible but safe code
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
 
// 3. Use union types for multiple specific types
function formatValue(value: string | number | boolean): string {
  return String(value);
}

Understanding the Type Hierarchy

TypeScript has a type hierarchy that's important to understand:

TYPESCRIPT
// Top types (supertypes of all types)
let anything: unknown;
let anyThing: any;
 
// Bottom type (subtype of all types)
let impossible: never;
 
// The hierarchy:
// unknown/any (top)
//     ↓
// object | primitive types
//     ↓
// specific types (string, number, interfaces, etc.)
//     ↓
// literal types ('hello', 42, etc.)
//     ↓
// never (bottom)

Note

The never type represents values that never occur. It's the return type of functions that always throw or have infinite loops.

TYPESCRIPT
function throwError(message: string): never {
  throw new Error(message);
}
 
function infiniteLoop(): never {
  while (true) {
    // ...
  }
}

Key Takeaways

  1. TypeScript is a superset of JavaScript - all JS is valid TS
  2. Types are erased at runtime - they have no runtime presence
  3. Use strict mode for new projects, especially strictNullChecks and noImplicitAny
  4. Structural typing determines compatibility based on shape, not name
  5. Avoid any - use unknown, generics, or union types instead
  6. Types serve as documentation that the compiler verifies

In the next chapter, we'll dive deeper into TypeScript's type system, exploring structural typing in more detail and learning about type narrowing.