Understanding 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:
// 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:
// 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 stringHowever, TypeScript also catches many common errors that JavaScript would silently accept:
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:
- Catch errors at compile time rather than runtime
- Provide better tooling (autocomplete, refactoring, navigation)
- Serve as documentation that stays in sync with the code
- Model JavaScript's runtime behavior accurately
Catching Errors Early
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:
{
"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
// 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
// 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 explicitItem 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
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
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:
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
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
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:
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 5When Structural Typing Causes Problems
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:
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
- No type safety: Any operation is allowed
- Breaks contracts: Functions expecting specific types get garbage
- No autocomplete: Editor can't help you
- Hides refactoring bugs: Changes won't be flagged
- No documentation: Readers can't understand the code
The Dangers of any
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
// 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:
// 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.
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// ...
}
}Key Takeaways
- TypeScript is a superset of JavaScript - all JS is valid TS
- Types are erased at runtime - they have no runtime presence
- Use strict mode for new projects, especially
strictNullChecksandnoImplicitAny - Structural typing determines compatibility based on shape, not name
- Avoid
any- useunknown, generics, or union types instead - 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.