TypeScript Introduction Tutorial

JavaScript with a Safety Net

Picture a codebase that's grown to fifty thousand lines across a dozen developers. Someone changes a function to accept an object instead of a string. Three files away, another function still passes a string. The bug is invisible until a user hits a specific edge case in production, and by then it's a support ticket and a late-night fix. TypeScript is the tool that catches that class of problem during development instead. It adds a type system on top of JavaScript that describes what shape each value should have, and the compiler checks those descriptions before any code runs.

What is TypeScript?

TypeScript is an open-source language developed and maintained by Microsoft. It's a superset of JavaScript - any valid JavaScript is also valid TypeScript, which means you can add it gradually to an existing project rather than rewriting from scratch. The type annotations you add are checked by the TypeScript compiler before compilation, then stripped out entirely - the output is plain JavaScript that runs in any browser or Node.js environment. The type system adds nothing at runtime and has no performance cost. What it adds is a layer of checking during development that catches mismatches, missing properties, incorrect function arguments, and similar mistakes before they become runtime errors.

TypeScript Core Properties

  • Superset of JavaScript - All valid JavaScript is valid TypeScript. You can migrate incrementally, file by file.
  • Statically typed - Types are checked before the code runs, catching mismatches during development.
  • Compiles to JavaScript - TypeScript compiles to plain JavaScript that runs anywhere JavaScript runs.
  • No runtime overhead - Type annotations are stripped during compilation. Nothing type-related exists at runtime.

A Brief History

TypeScript came from Microsoft engineers dealing with large-scale JavaScript applications, led by Anders Hejlsberg - the same person who created C# and Delphi. The first public release was in 2012. The 0.9 release in 2013 added generics. Version 1.0 arrived in 2014. The real adoption inflection point was 2016 when Angular 2 chose TypeScript as its primary language, which pulled a large portion of the frontend community along with it. Stack Overflow developer surveys from 2020 onward consistently placed TypeScript among the most loved languages. Companies like Google, Airbnb, and Slack adopted it widely. The language now updates on a roughly quarterly release cycle, adding features like template literal types, satisfies operator, and improved narrowing with each version.

Key Milestones

  • 2012 - First public release by Microsoft.
  • 2013 - Version 0.9 adds generics.
  • 2014 - Version 1.0 stable release.
  • 2016 - Angular 2 adopts TypeScript - major adoption inflection point.
  • 2020+ - Mainstream adoption. Consistently voted most loved language in developer surveys.

How TypeScript Works

TypeScript is a development-time tool. You write .ts or .tsx files with type annotations. The TypeScript compiler (tsc) reads those files, checks that all the types are consistent, and if they are, outputs .js files. If there are type errors, it reports them and you fix them before the code ships. The compiled JavaScript is clean and readable - it doesn't have any TypeScript-specific wrappers or overhead. One thing worth understanding: TypeScript's checking happens entirely at compile time. At runtime, the code is ordinary JavaScript with no type awareness. This is why TypeScript can catch type mismatches but can't catch runtime values that violate types unless you write validation code yourself.

The Compilation Process

  • Write .ts files - Add type annotations to your code alongside regular JavaScript.
  • Type checking - tsc analyzes types before compilation and reports any mismatches.
  • Compilation - Error-free code compiles to plain .js files with type annotations removed.
  • Execution - Compiled JavaScript runs in any environment exactly like hand-written JavaScript.

Core Concepts: Basic Types

TypeScript's type annotations use a colon followed by the type name. For variables where TypeScript can infer the type from the initial value, you don't need to write the annotation explicitly. The any type opts out of type checking for that variable - it's an escape hatch, not a default. Avoid it when possible. Tuple types define arrays where position matters and each position has a specific type. Enums give names to sets of related constants.

typescript
1// Explicit type annotations
2let userName: string = "Alice";
3let userCount: number = 42;
4let isActive: boolean = true;
5
6// Arrays
7let colors: string[] = ["red", "green", "blue"];
8let scores: Array<number> = [95, 87, 92];
9
10// any - opt out of checking (avoid where possible)
11let dynamic: any = "text";
12dynamic = 42; // No error - any disables checking
13
14// Tuple - fixed positions with specific types
15let person: [string, number] = ["Alice", 30];
16// [30, "Alice"] would be a type error
17
18// Enum - named constants
19enum Color { Red, Green, Blue }
20let favoriteColor: Color = Color.Green;
21console.log(Color.Green); // 1 (enums are numeric by default)

Basic Types

  • string, number, boolean - The three most common primitive types. TypeScript infers these from initial values.
  • string[] or Array<string> - Two syntaxes for the same thing - an array of strings. Both work.
  • any - Disables type checking for that variable. Use as a last resort during migrations, not as a default.
  • tuple - Array with a fixed number of elements where each position has a known type.
  • enum - Named constants. Numeric by default (0, 1, 2...) or string enums if you assign string values.

Core Concepts: Interfaces

Interfaces describe the shape of an object - what properties it must have and what types those properties should be. Optional properties are marked with a question mark. Readonly properties can be set during creation but not changed afterward. Interfaces can also describe function signatures. When you pass an object to a function that expects a typed parameter, TypeScript checks that the object has all the required properties with the right types - this is called structural typing, and it means you don't have to explicitly declare that an object implements an interface.

typescript
1interface User {
2  id: number;
3  name: string;
4  email: string;
5  age?: number;           // Optional - may or may not be present
6  readonly createdAt: Date; // Can be set once, never changed
7}
8
9const currentUser: User = {
10  id: 1,
11  name: "Alice",
12  email: "alice@example.com",
13  createdAt: new Date()
14};
15
16// currentUser.createdAt = new Date(); // Error: readonly
17// currentUser.id = 2;                  // Error: readonly... no wait, id is not readonly
18// currentUser.unknownProp = "test";    // Error: not in interface
19
20// Interfaces for function signatures
21interface SearchFn {
22  (source: string, term: string): boolean;
23}
24
25const search: SearchFn = function(src, sub) {
26  return src.includes(sub);
27};
28
29// Extending interfaces
30interface AdminUser extends User {
31  permissions: string[];
32}

Interface Features

  • Required properties - Properties without ? must be present and have the correct type.
  • Optional properties (?) - May be present or absent. TypeScript won't complain if they're missing.
  • Readonly properties - Can be assigned during object creation but cannot be changed afterward.
  • Structural typing - TypeScript checks shape, not declaration. An object satisfies an interface if it has the right properties, even without explicitly saying so.

Core Concepts: Type Inference

TypeScript infers types from context, which means you don't need to annotate everything. Assign a string to a variable and TypeScript knows it's a string - writing : string is redundant. Function return types are often inferred from the return statement. Event handler parameters are inferred from the event type. The practical guideline: write explicit annotations for function parameters (TypeScript can't infer those without context) and for variables declared without an initial value. Let TypeScript handle the rest.

typescript
1// TypeScript infers from initial values - no annotation needed
2let message = "Hello"; // TypeScript knows: string
3let count = 42;         // TypeScript knows: number
4let active = true;      // TypeScript knows: boolean
5let tags = ["ts", "js"]; // TypeScript knows: string[]
6
7// message = 42; // Error - TypeScript inferred string, 42 is not string
8
9// Return types inferred from the return statement
10function add(a: number, b: number) {
11    return a + b; // return type inferred as number
12}
13
14// Contextual typing for event handlers
15document.addEventListener("click", (event) => {
16    console.log(event.clientX); // TypeScript knows event is MouseEvent
17    // console.log(event.key);  // Error - MouseEvent has no .key
18});
19
20// When to be explicit: function parameters, uninitialized variables
21let result: string; // Declare without value - annotation needed
22result = "done";

When Inference Works

  • Variable initialization - Type is inferred from the assigned value. No annotation needed.
  • Function return types - Inferred from what the function returns. Explicit annotation is optional but can be useful as documentation.
  • Contextual typing - Callback parameters pick up their types from the context - event handlers, array method callbacks.
  • When to annotate explicitly - Function parameters and variables declared without an initial value need explicit annotations.

Core Concepts: Union and Literal Types

Union types let a variable hold more than one type, written with a pipe separator. Literal types restrict a variable to specific values - the string 'north' is a narrower type than string in general. Combining them lets you create something like an enum but in a lighter syntax. When you use a union type in a conditional, TypeScript narrows the type inside each branch - if you check typeof value === 'string', TypeScript knows it's a string within that branch and gives you string methods.

typescript
1// Union types - can be string or number
2let id: string | number;
3id = "user-123"; // OK
4id = 123;        // OK
5// id = true;    // Error: boolean not in union
6
7// Narrowing in conditionals
8function formatId(id: string | number): string {
9    if (typeof id === 'string') {
10        return id.toUpperCase(); // TypeScript knows id is string here
11    }
12    return id.toString();       // TypeScript knows id is number here
13}
14
15// Literal types - constrained to specific values
16type Direction = "north" | "south" | "east" | "west";
17let move: Direction = "north"; // OK
18// move = "up";                // Error: not in the union
19
20// Useful for function parameters
21function getArea(shape: "circle" | "square", size: number): number {
22    if (shape === "circle") {
23        return Math.PI * size * size;
24    }
25    return size * size;
26}
27
28getArea("circle", 5);    // OK
29// getArea("triangle", 5); // Error: not a valid shape

Union and Literal Types

  • Union (|) - A value can be one of several types. string | number means either is valid.
  • Literal types - Constrain a variable to specific values: 'north' | 'south' only accepts those two strings.
  • Type narrowing - TypeScript narrows the type inside conditionals. After typeof check, it knows which branch has which type.
  • type alias - The type keyword creates a named alias for a type, including complex unions.

Core Concepts: Generics

Generics let you write functions and types that work with any type while still preserving type information. Without generics, a function that returns its input would have to return any, losing all type information. With a generic type parameter T, the function can return the same type it received. Generic interfaces are common in API response patterns where the outer structure is always the same but the data inside varies by endpoint.

typescript
1// Without generics: loses type information
2// function identity(arg: any): any { return arg; }
3
4// With generics: preserves type information
5function identity<T>(arg: T): T {
6    return arg;
7}
8
9const str = identity("hello");  // TypeScript knows str is string
10const num = identity(42);        // TypeScript knows num is number
11// identity<string>(42);         // Error: 42 is not string
12
13// Generic interface for API responses
14interface ApiResponse<T> {
15    data: T;
16    status: number;
17    message: string;
18}
19
20// Same wrapper, different data shapes
21const userRes: ApiResponse<User> = {
22    data: { id: 1, name: "Alice", email: "a@ex.com", createdAt: new Date() },
23    status: 200,
24    message: "OK"
25};
26
27const listRes: ApiResponse<string[]> = {
28    data: ["item1", "item2"],
29    status: 200,
30    message: "OK"
31};

Why Generics

  • Type variable T - A placeholder for a type that gets filled in at the call site. T is convention but any name works.
  • Type inference with generics - TypeScript infers T from the argument, so you rarely need to specify it explicitly.
  • Reusability without any - Write code once that works with multiple types, without losing type information the way any does.
  • Generic interfaces - Describe structures that wrap different data types - API responses, containers, result types.

TypeScript Configuration

The tsconfig.json file at the root of a project controls what TypeScript checks and how it compiles. The strict option is a single flag that enables a group of checks that are off by default - turning it on is recommended for new projects. The target option controls which JavaScript version the output targets. You can configure separate source and output directories, which files to include, and how to handle module imports.

json
1// tsconfig.json
2{
3  "compilerOptions": {
4    "target": "ES2020",          // Output JS version
5    "module": "commonjs",        // Module system
6    "lib": ["DOM", "ES2020"],    // Available APIs
7    "outDir": "./dist",          // Compiled JS goes here
8    "rootDir": "./src",          // Source files here
9    "strict": true,              // Recommended: enables strict checks
10    "esModuleInterop": true,     // Better import compatibility
11    "skipLibCheck": true,        // Skip type checking of .d.ts files
12    "forceConsistentCasingInFileNames": true
13  },
14  "include": ["src/**/*"],
15  "exclude": ["node_modules", "dist"]
16}

Key Compiler Options

  • strict - Umbrella flag enabling strictNullChecks, noImplicitAny, and other strict checks. Enable for new projects.
  • target - The JavaScript version to compile to. ES2020 is a safe modern choice for most projects.
  • outDir / rootDir - Separate compiled output from source files. Standard project structure.
  • include / exclude - Control which files TypeScript processes. Glob patterns work.

Why Use TypeScript?

The most immediate benefit is catching type-related mistakes before they reach production. A function that receives the wrong type of argument, a missing property access, a null check that was forgotten - TypeScript surfaces these during development rather than when a user hits them. The secondary benefit is tooling: because editors like VS Code understand the type system, they can offer accurate autocomplete, inline documentation, and refactoring operations that work across an entire codebase. Renaming a function in TypeScript updates every call site. Finding all usages of a type is instant. The third benefit matters most in teams: type annotations serve as always-accurate documentation of what each function expects and returns, which reduces the mental overhead of working with code someone else wrote.

Main Benefits

  • Early error detection - Type mismatches, missing properties, null issues - caught before runtime.
  • IDE intelligence - Accurate autocomplete, inline docs, safe rename and refactor across the codebase.
  • Living documentation - Type annotations describe what functions expect and return - always in sync with the code.
  • Gradual adoption - Can be added to existing JavaScript projects incrementally. No big-bang rewrite required.

TypeScript in Practice

TypeScript doesn't replace JavaScript - it's JavaScript with an additional layer of checking that gets stripped away before the code runs. The learning curve is real but shallow: basic type annotations are intuitive, and the more advanced features (generics, conditional types, mapped types) can be learned as you encounter the problems they solve rather than all upfront. The practical starting point is adding TypeScript to a small project with strict mode enabled, letting the compiler guide you to the annotations you need. Most developers who try it stop wanting to go back

Frequently Asked Questions

Do I need to learn JavaScript before TypeScript?

Yes. TypeScript is JavaScript with type annotations - without understanding JavaScript you'd be learning two things at once, and the harder one would be obscured. Learn JavaScript to the point where you understand scope, functions, objects, and asynchronous patterns. Then TypeScript's annotations make sense because you understand what they're describing.

Does TypeScript make code run faster?

No. TypeScript types are erased during compilation and don't exist at runtime. The JavaScript that runs is identical in performance to JavaScript you'd write by hand. The benefit is development speed and reliability, not execution speed.

Is TypeScript worth it for small projects?

For a quick throwaway script or a weekend prototype, probably not - the setup overhead isn't worth it. For anything you plan to maintain or return to, even a few months later, the type annotations pay off by making the code self-documenting and reducing the time spent remembering what each function expects.

Can I use TypeScript with React, Vue, or Angular?

Yes. All three have first-class TypeScript support. Angular is built with TypeScript. Create React App includes a TypeScript template. Vue 3 was rewritten in TypeScript and has excellent TS support. For new projects with any of these frameworks, starting with TypeScript is the standard recommendation.

How do I migrate an existing JavaScript project to TypeScript?

Rename .js files to .ts one at a time. TypeScript will infer types where it can and flag the rest. Start with allowJs and loose settings in tsconfig to reduce the initial noise, then tighten them as you add annotations. Using any liberally during migration is acceptable - the goal is to get TypeScript checking in place and then replace any with real types gradually.

What's the difference between interface and type?

For object shapes, they're mostly interchangeable. Interfaces can be extended with the extends keyword and can be merged (two declarations of the same interface name combine). Type aliases are more flexible for everything else - unions, intersections, mapped types, template literal types. The common advice: use interface for object shapes and class contracts, use type for everything else.