TypeScript: Complete Developer Learning Guide
From JavaScript fundamentals to advanced type systems, modern development, and real-world frontend integration.
Introduction
Modern web applications require maintainable, scalable, and reliable code. As applications grow, managing JavaScript code becomes increasingly challenging, especially when multiple developers work on the same project.
TypeScript helps solve this problem by adding a static type system to JavaScript. It allows developers to define types, identify potential errors during development, improve code readability, and build more maintainable applications.
For a .NET developer familiar with C#, TypeScript provides a familiar development experience through interfaces, classes, generics, access modifiers, inheritance, and type checking.
However, TypeScript is not C#. It builds on JavaScript, and its types are primarily checked during development and compilation. Type annotations are generally removed when TypeScript is transformed into JavaScript.
What Is TypeScript?
TypeScript is an open-source programming language developed by Microsoft. It extends JavaScript with static typing and additional development-time features.
TypeScript code is transformed into JavaScript so it can run in JavaScript environments such as browsers and Node.js.
Why Use TypeScript?
- Static type checking: Detect many programming errors before runtime.
- Type inference: Automatically determine types from values and expressions.
- Better IntelliSense: Get code completion and helpful editor information.
- Maintainability: Make large applications easier to understand and modify.
- Refactoring: Safely rename properties, functions, and types with editor support.
- Object-oriented programming: Use classes, interfaces, inheritance, and generics.
- JavaScript compatibility: Use existing JavaScript libraries and ecosystem tools.
Where Is TypeScript Used?
- Angular applications.
- React applications.
- Vue applications.
- Node.js backend applications.
- Full-stack web applications.
- Enterprise frontend applications.
- Shared libraries and SDK development.
TypeScript vs JavaScript vs C#
| Feature | JavaScript | TypeScript | C# |
|---|---|---|---|
| Typing | Dynamic | Static checking with inference | Static |
| Execution | JavaScript runtime | Compiled/transformed to JavaScript | .NET runtime |
| Interfaces | No native TS-style interfaces | Yes | Yes |
| Classes | Yes | Yes | Yes |
| Generics | No static generic type system | Yes | Yes |
| Type checking | Primarily runtime behavior | Development/compile time | Compile time |
| Primary ecosystem | Web and server-side JavaScript | Typed JavaScript development | .NET applications |
JavaScript Example
function add(a, b) {
return a + b;
}
console.log(add(10, 20));
JavaScript does not require the developer to declare the expected parameter types.
TypeScript Example
function add(a: number, b: number): number {
return a + b;
}
console.log(add(10, 20));
TypeScript checks that both parameters are numbers and that the function returns a number.
Equivalent C# Example
public int Add(int a, int b)
{
return a + b;
}
How TypeScript Works
TypeScript adds a development-time type system to JavaScript. The TypeScript compiler checks the code and can emit JavaScript output.
Basic Workflow
TypeScript Source (.ts)
|
v
Type Checking
|
v
TypeScript Compiler
|
v
JavaScript Output (.js)
|
v
Browser / Node.js Runtime
For example, a TypeScript file named app.ts can be transformed into JavaScript.
// app.ts
const message: string = "Hello TypeScript";
console.log(message);
Possible JavaScript output:
// app.js
const message = "Hello TypeScript";
console.log(message);
The type annotation : string does not remain in the emitted JavaScript.
Important Concepts
- TypeScript is not a separate runtime like .NET CLR.
- JavaScript is the runtime language.
- Type annotations do not normally create runtime validation.
- TypeScript can report errors without necessarily preventing JavaScript emission, depending on compiler settings.
- Build tools such as Vite, Angular CLI, and other bundlers can handle TypeScript as part of their build process.
Development Environment and Installation
You can write TypeScript using Visual Studio Code, Visual Studio, or another editor with TypeScript language support.
Step 1: Install Node.js
Install a supported Node.js version from the official website:
Step 2: Check Node.js and npm
node --version
npm --version
Step 3: Create a Project
mkdir TypeScriptLearning
cd TypeScriptLearning
npm init -y
Step 4: Install TypeScript
npm install --save-dev typescript
Install TypeScript as a project-local development dependency. The official TypeScript site currently lists TypeScript 7.0 as the latest stable release and 7.1 as beta. For production work, use the stable version supported by your framework, editor, and build tooling, and commit the package lockfile so the team uses a consistent version.
Step 5: Verify Installation
npx tsc --version
Step 6: Create a TypeScript File
Create app.ts:
const developerName: string = "Alex";
console.log(`Welcome ${developerName}`);
Step 7: Compile the File
npx tsc app.ts
This produces a JavaScript file that can run in a compatible JavaScript environment.
Step 8: Execute JavaScript
node app.js
TypeScript Project Structure
A simple TypeScript application can use the following structure:
TypeScriptLearning/
│
├── src/
│ ├── models/
│ │ └── employee.model.ts
│ │
│ ├── services/
│ │ └── employee.service.ts
│ │
│ ├── utils/
│ │ └── helper.ts
│ │
│ └── app.ts
│
├── dist/
│
├── package.json
├── package-lock.json
└── tsconfig.json
| Folder/File | Purpose |
|---|---|
| src | Contains TypeScript source code. |
| models | Contains application data models and interfaces. |
| services | Contains business logic and service classes. |
| utils | Contains reusable helper functions. |
| dist | Contains generated build output when configured. |
| package.json | Stores project metadata, dependencies, and scripts. |
| tsconfig.json | Stores TypeScript compiler configuration. |
Basic Types and Type Inference
Types describe the kind of values a variable, parameter, property, or function can use.
String
let developerName: string = "Alex";
Number
let age: number = 30;
let salary: number = 75000;
TypeScript uses the number type for both integer-like and floating-point values.
Boolean
let isActive: boolean = true;
Null and Undefined
let employeeName: string | null = null;
let department: string | undefined;
Any
let data: any = "Hello";
data = 100;
data = true;
The any type disables many useful type checks for that value. Avoid using it unnecessarily.
Unknown
let response: unknown = "Success";
if (typeof response === "string") {
console.log(response.toUpperCase());
}
unknown is safer than any because TypeScript requires you to narrow the type before performing operations that depend on it.
Void
function displayMessage(): void {
console.log("Welcome");
}
Never
function throwError(message: string): never {
throw new Error(message);
}
never represents values that cannot occur, such as a function that always throws an exception or never completes normally.
Type Inference
TypeScript can automatically determine types from assigned values.
let city = "Delhi";
let count = 10;
let isAvailable = true;
The inferred types are string, number, and boolean.
Variables and Constants
TypeScript supports JavaScript variable declarations.
let
let age: number = 30;
age = 31;
Use let when reassignment is required.
const
const companyName: string = "Example Technologies";
Use const when the variable binding should not be reassigned.
var
var employeeId: number = 101;
var is function-scoped and has older JavaScript behavior. Prefer let and const in modern applications.
Readonly Properties
interface Employee {
readonly id: number;
name: string;
}
const employee: Employee = {
id: 101,
name: "Alex"
};
// employee.id = 102; // TypeScript error
readonly prevents reassignment through the typed reference. It does not automatically freeze the underlying JavaScript object at runtime.
Arrays and Tuples
Array of Numbers
let numbers: number[] = [10, 20, 30];
Array of Strings
let names: string[] = ["Alex", "John", "David"];
Generic Array Syntax
let salaries: Array<number> = [30000, 45000, 60000];
Array of Objects
interface Employee {
id: number;
name: string;
}
const employees: Employee[] = [
{ id: 1, name: "Alex" },
{ id: 2, name: "John" }
];
Tuples
A tuple represents an array with a known sequence of element types.
let employee: [number, string, boolean];
employee = [101, "Alex", true];
Here, the first element must be a number, the second a string, and the third a boolean.
Readonly Arrays
const roles: readonly string[] = [
"Admin",
"Developer",
"Manager"
];
Readonly array types prevent modifying the array through that reference.
Objects, Interfaces and Type Aliases
Object Types
let employee: {
id: number;
name: string;
isActive: boolean;
} = {
id: 101,
name: "Alex",
isActive: true
};
Interfaces
An interface defines the expected structure of an object.
interface Employee {
id: number;
name: string;
email: string;
salary: number;
}
const employee: Employee = {
id: 101,
name: "Alex",
email: "alex@example.com",
salary: 50000
};
Optional Properties
interface Employee {
id: number;
name: string;
department?: string;
}
The ? symbol means that the property is optional.
Readonly Properties
interface Product {
readonly productId: number;
productName: string;
}
Extending Interfaces
interface Person {
name: string;
age: number;
}
interface Developer extends Person {
programmingLanguage: string;
}
const developer: Developer = {
name: "Alex",
age: 30,
programmingLanguage: "C#"
};
Type Aliases
A type alias creates a name for a type.
type EmployeeId = number;
type Employee = {
id: EmployeeId;
name: string;
department: string;
};
const employee: Employee = {
id: 101,
name: "Alex",
department: "IT"
};
Interface vs Type Alias
| Interface | Type Alias |
|---|---|
| Primarily describes object contracts. | Can describe objects, unions, tuples, primitives, and more. |
| Supports declaration merging. | Cannot be redeclared to merge in the same way. |
| Can extend other interfaces. | Can combine types using intersections. |
| Commonly used for object shapes and contracts. | Useful for complex type expressions. |
Union and Intersection Types
Union Types
A union allows a value to belong to one of several types.
let employeeId: number | string;
employeeId = 101;
employeeId = "EMP101";
Union with Function Parameters
function displayId(id: number | string): void {
console.log(id);
}
displayId(101);
displayId("EMP101");
Intersection Types
An intersection combines multiple types into one type.
type Person = {
name: string;
};
type Employee = {
employeeId: number;
};
type Developer = Person & Employee;
const developer: Developer = {
name: "Alex",
employeeId: 101
};
Enums and Literal Types
Numeric Enum
enum Status {
Pending,
Approved,
Rejected
}
let currentStatus: Status = Status.Approved;
String Enum
enum UserRole {
Admin = "ADMIN",
Manager = "MANAGER",
Employee = "EMPLOYEE"
}
Literal Types
type OrderStatus =
"Pending" |
"Processing" |
"Completed" |
"Cancelled";
let status: OrderStatus = "Pending";
Discriminated Unions
Discriminated unions are useful for representing different states of an operation.
type ApiResult =
| { status: "success"; data: string }
| { status: "error"; message: string }
| { status: "loading" };
function handleResult(result: ApiResult): void {
switch (result.status) {
case "success":
console.log(result.data);
break;
case "error":
console.log(result.message);
break;
case "loading":
console.log("Loading...");
break;
}
}
Functions and Function Types
Basic Function
function add(a: number, b: number): number {
return a + b;
}
Optional Parameters
function greet(name: string, title?: string): string {
return title
? `Hello ${title} ${name}`
: `Hello ${name}`;
}
Default Parameters
function calculateSalary(
basicSalary: number,
bonus: number = 0
): number {
return basicSalary + bonus;
}
Rest Parameters
function total(...numbers: number[]): number {
return numbers.reduce(
(sum, current) => sum + current,
0
);
}
console.log(total(10, 20, 30));
Arrow Functions
const multiply = (a: number, b: number): number => {
return a * b;
};
Function Type
type Calculation = (a: number, b: number) => number;
const subtract: Calculation = (a, b) => a - b;
Function Overloading
function getValue(value: string): string;
function getValue(value: number): number;
function getValue(value: string | number): string | number {
return value;
}
getValue("Hello");
getValue(100);
Function overload signatures describe the supported calls, while the implementation handles the combined input types.
Type Narrowing and Type Guards
Type narrowing allows TypeScript to understand a more specific type after checking a value.
typeof
function printValue(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
instanceof
class Admin {
manageUsers(): void {
console.log("Managing users");
}
}
function checkUser(user: Admin | Date): void {
if (user instanceof Admin) {
user.manageUsers();
}
}
in Operator
type AdminUser = {
role: "admin";
permissions: string[];
};
type RegularUser = {
role: "user";
email: string;
};
function processUser(user: AdminUser | RegularUser): void {
if ("permissions" in user) {
console.log(user.permissions);
} else {
console.log(user.email);
}
}
Custom Type Guard
interface Employee {
id: number;
name: string;
}
function isEmployee(value: unknown): value is Employee {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value as Record;
return (
typeof candidate.id === "number" &&
typeof candidate.name === "string"
);
}
Custom type guards are useful when checking unknown values, although production validation may require checking every property and its actual runtime type.
Type Assertions
const input = document.getElementById("username")
as HTMLInputElement | null;
if (input) {
console.log(input.value);
}
Classes and Object-Oriented Programming
TypeScript supports classes and familiar object-oriented programming concepts.
Class Example
class Employee {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
display(): void {
console.log(`${this.id} - ${this.name}`);
}
}
const employee = new Employee(101, "Alex");
employee.display();
Access Modifiers
| Modifier | Meaning |
|---|---|
| public | Accessible from anywhere; default visibility. |
| private | Restricted to the declaring class under TypeScript's type checking. |
| protected | Accessible within the class and its subclasses. |
| readonly | Prevents reassignment after initialization, subject to its initialization rules. |
Inheritance
class Person {
constructor(public name: string) {}
displayName(): void {
console.log(this.name);
}
}
class Developer extends Person {
constructor(
name: string,
public language: string
) {
super(name);
}
displayLanguage(): void {
console.log(this.language);
}
}
Abstract Classes
abstract class Shape {
abstract calculateArea(): number;
display(): void {
console.log("Shape calculation");
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super();
}
calculateArea(): number {
return this.width * this.height;
}
}
Implementing Interfaces
interface ILogger {
log(message: string): void;
}
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(message);
}
}
Parameter Properties
class Product {
constructor(
public id: number,
public name: string,
private price: number
) {}
}
Parameter properties allow constructor parameters to declare and initialize class properties together.
Generics
Generics allow developers to create reusable functions, interfaces, and classes that work with multiple types while preserving type information.
For a C# developer, TypeScript generics are conceptually similar to C# generics.
Generic Function
function identity<T>(value: T): T {
return value;
}
const numberValue = identity<number>(100);
const stringValue = identity<string>("Hello");
Generic Interface
interface Repository<T> {
getById(id: number): T | undefined;
getAll(): T[];
}
interface Employee {
id: number;
name: string;
}
Generic Class
class DataStore<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
const employeeStore = new DataStore<Employee>();
Generic Constraints
function getLength<T extends { length: number }>(
value: T
): number {
return value.length;
}
getLength("TypeScript");
getLength([10, 20, 30]);
keyof with Generics
function getProperty<T, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
const employee = {
id: 101,
name: "Alex"
};
const name = getProperty(employee, "name");
Generic API Response
interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
interface Employee {
id: number;
name: string;
}
const response: ApiResponse<Employee> = {
success: true,
message: "Employee retrieved",
data: {
id: 101,
name: "Alex"
}
};
Advanced Type Manipulation
TypeScript provides type operators and advanced type constructs that allow developers to build reusable and expressive type definitions.
keyof Operator
type Employee = {
id: number;
name: string;
department: string;
};
type EmployeeKeys = keyof Employee;
// "id" | "name" | "department"
typeof Operator
const employee = {
id: 101,
name: "Alex"
};
type Employee = typeof employee;
Indexed Access Types
type Employee = {
id: number;
name: string;
salary: number;
};
type EmployeeName = Employee["name"];
type EmployeeValues = Employee[keyof Employee];
Conditional Types
type IsString<T> = T extends string ? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
Mapped Types
type Employee = {
id: number;
name: string;
};
type OptionalEmployee = {
[K in keyof Employee]?: Employee[K];
};
Template Literal Types
type EventName = "click" | "change";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onChange"
as const
const roles = ["Admin", "Manager", "Employee"] as const;
type Role = typeof roles[number];
// "Admin" | "Manager" | "Employee"
satisfies Operator
The satisfies operator checks that an expression matches a type without replacing the expression's more specific inferred type.
type AppConfig = {
apiUrl: string;
retryCount: number;
};
const config = {
apiUrl: "/api",
retryCount: 3
} satisfies AppConfig;
const Type Parameters
Const type parameters help preserve narrow literal types during generic type inference.
function createList<const T extends readonly string[]>(
values: T
): T {
return values;
}
const roles = createList(["Admin", "Manager"]);
// Preserves the literal tuple type.
Utility Types
TypeScript provides built-in utility types that transform existing types without requiring developers to write the transformation manually.
Partial
interface Employee {
id: number;
name: string;
department: string;
}
type UpdateEmployee = Partial<Employee>;
All properties become optional.
Required
type RequiredEmployee = Required<Partial<Employee>>;
All properties become required.
Readonly
type ReadonlyEmployee = Readonly<Employee>;
Pick
type EmployeeSummary = Pick<Employee, "id" | "name">;
Omit
type EmployeeWithoutDepartment =
Omit<Employee, "department">;
Record
type EmployeeRoles = Record<number, string>;
const roles: EmployeeRoles = {
101: "Admin",
102: "Developer"
};
Exclude
type Status = "Pending" | "Approved" | "Rejected";
type ActiveStatus = Exclude<Status, "Rejected">;
Extract
type SelectedStatus = Extract<Status, "Approved" | "Rejected">;
NonNullable
type EmployeeName = NonNullable<string | null | undefined>;
ReturnType
function getEmployee() {
return {
id: 101,
name: "Alex"
};
}
type Employee = ReturnType<typeof getEmployee>;
Parameters
function createEmployee(
id: number,
name: string
): void {}
type EmployeeParameters =
Parameters<typeof createEmployee>;
Awaited
type Employee = {
id: number;
name: string;
};
type Result = Awaited<Promise<Employee>>;
Modules and Namespaces
Modules
Modules allow code to be separated into files and reused through imports and exports.
employee.model.ts
export interface Employee {
id: number;
name: string;
}
employee.service.ts
import { Employee } from "./employee.model";
export function getEmployee(): Employee {
return {
id: 101,
name: "Alex"
};
}
Default Export
export default class EmployeeService {
getEmployees(): string[] {
return ["Alex", "John"];
}
}
Import:
import EmployeeService from "./employee.service";
Named Export
export const apiUrl = "/api";
export function getMessage(): string {
return "Hello";
}
Dynamic Import
async function loadModule() {
const module = await import("./employee.service");
console.log(module);
}
Namespaces
namespace EmployeeManagement {
export interface Employee {
id: number;
name: string;
}
export function display(employee: Employee): void {
console.log(employee.name);
}
}
Modern TypeScript applications generally use ES modules with import and export. Namespaces remain available for particular legacy and declaration scenarios.
Asynchronous Programming
Modern web applications frequently communicate with APIs, databases through backend services, and other external systems. Asynchronous programming allows operations to complete without blocking the main JavaScript execution flow.
Promise
const getData = (): Promise<string> => {
return Promise.resolve("Data received");
};
getData().then(result => {
console.log(result);
});
async and await
async function getEmployee(): Promise<string> {
return "Alex";
}
async function displayEmployee(): Promise<void> {
const employee = await getEmployee();
console.log(employee);
}
Fetch API
interface Employee {
id: number;
name: string;
}
async function getEmployees(): Promise<Employee[]> {
const response = await fetch("/api/employees");
if (!response.ok) {
throw new Error("Unable to retrieve employees");
}
return response.json() as Promise<Employee[]>;
}
Promise.all
const [employees, departments] = await Promise.all([
fetch("/api/employees"),
fetch("/api/departments")
]);
Promise.all waits for all promises to fulfill, or rejects when one rejects.
Promise.allSettled
const results = await Promise.allSettled([
fetch("/api/employees"),
fetch("/api/departments")
]);
Unlike Promise.all, it reports the outcome of each promise independently.
Error Handling
try-catch-finally
async function loadData(): Promise<void> {
try {
const response = await fetch("/api/employees");
if (!response.ok) {
throw new Error("Request failed");
}
const data = await response.json();
console.log(data);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
} finally {
console.log("Request completed");
}
}
Custom Error Class
class ApiError extends Error {
constructor(
message: string,
public statusCode: number
) {
super(message);
this.name = "ApiError";
}
}
throw new ApiError("Employee not found", 404);
Result Type Pattern
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function processResult<T>(result: Result<T>): void {
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
}
Modern TypeScript Features
TypeScript continues to evolve. Modern TypeScript development includes features that make type definitions more expressive and improve compatibility with the JavaScript ecosystem.
Important Modern Features
| Feature | Purpose |
|---|---|
| Optional chaining (?.) | Safely access nested properties when values may be null or undefined. |
| Nullish coalescing (??) | Provide a fallback only when a value is null or undefined. |
| Template literal types | Create types from string patterns. |
| as const | Preserve literal types and readonly information. |
| satisfies | Check compatibility while retaining specific inferred types. |
| Const type parameters | Preserve literal information in generic inference. |
| Modern decorators | Support the standardized decorator model. |
| Import attributes | Attach metadata to module imports where supported by the environment. |
| Native TypeScript compiler | TypeScript 7 uses a Go-based native compiler implementation. |
Optional Chaining
const employee = {
name: "Alex",
address: {
city: "Delhi"
}
};
console.log(employee.address?.city);
Nullish Coalescing
const department = null;
const result = department ?? "IT";
Nullish Coalescing vs OR
const count = 0;
const a = count || 10; // 10
const b = count ?? 10; // 0
Using Declarations
Modern TypeScript supports explicit resource management syntax such as using and await using, subject to the target and runtime support.
function processFile() {
using resource = createDisposableResource();
// Work with resource.
// Its disposal is handled at the end of the scope.
}
This feature relies on disposable resource protocols and appropriate runtime support or polyfills. It is not a replacement for ordinary garbage collection.
TypeScript 6 and TypeScript 7
TypeScript 6.0 was an important transition release that prepared the ecosystem for TypeScript 7. TypeScript 7 introduced a native Go-based compiler implementation intended to improve compiler and language-service performance.
When upgrading projects, review compiler options, build tools, libraries, and compatibility requirements rather than assuming that every existing project can upgrade without changes.
Decorators
Decorators are special functions that can add behavior or metadata to classes and class members.
Modern TypeScript supports the standardized decorator model. It is important to distinguish it from the older experimental decorator model used by some existing frameworks.
Simple Class Decorator
function logged<T extends new (...args: any[]) => object>(
target: T,
context: ClassDecoratorContext
) {
console.log(`Class: ${String(context.name)}`);
return target;
}
@logged
class EmployeeService {
getEmployees(): string[] {
return ["Alex", "John"];
}
}
Decorator support and syntax depend on the TypeScript version, compiler configuration, and framework conventions.
@Component, @Injectable, and @Input. Do not assume that Angular's decorator configuration is interchangeable with every example of standard TypeScript decorators.Compiler Configuration and tsconfig.json
The tsconfig.json file defines how TypeScript checks and compiles a project.
Create tsconfig.json
npx tsc --init
Example Configuration
{
"compilerOptions": {
"target": "ES2025",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"noEmitOnError": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"forceConsistentCasingInFileNames": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
This is an illustrative configuration for a modern bundler-based project, not a universal configuration for every framework or runtime.
Important Compiler Options
| Option | Purpose |
|---|---|
| target | Controls the JavaScript language level used for emitted output. |
| module | Controls the module format. |
| moduleResolution | Defines how imports are resolved. |
| strict | Enables the strict family of type-checking options. |
| noImplicitAny | Reports cases where an implicit any type would be inferred. |
| strictNullChecks | Checks null and undefined more carefully. |
| noUncheckedIndexedAccess | Adds undefined to types when accessing potentially missing indexed values. |
| noEmitOnError | Prevents output emission when errors are reported. |
| outDir | Defines the output directory. |
| rootDir | Defines the root source directory. |
| declaration | Generates declaration files for supported library workflows. |
| sourceMap | Generates source maps for debugging. |
Compile a Project
npx tsc
Watch for Changes
npx tsc --watch
TypeScript with Angular
Angular uses TypeScript as its primary application development language. TypeScript provides the type system for components, services, models, forms, and application logic.
Angular Component
import { Component } from '@angular/core';
@Component({
selector: 'app-employee',
standalone: true,
template: `
<h2>{{ employeeName }}</h2>
`
})
export class EmployeeComponent {
employeeName: string = "Alex";
}
Angular Interface
export interface Employee {
id: number;
name: string;
department: string;
}
Angular Service
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Employee {
id: number;
name: string;
}
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
private http = inject(HttpClient);
getEmployees(): Observable<Employee[]> {
return this.http.get<Employee[]>(
'/api/employees'
);
}
}
Angular Signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">
Increment
</button>
`
})
export class CounterComponent {
count = signal(0);
increment(): void {
this.count.update(value => value + 1);
}
}
Angular uses TypeScript types alongside its component system, dependency injection, RxJS, signals, and template type checking.
TypeScript with React
React applications can use TypeScript through TSX files, which combine TypeScript with JSX syntax.
React Component with Props
interface EmployeeProps {
id: number;
name: string;
}
function EmployeeCard({
id,
name
}: EmployeeProps) {
return (
<div>
<h2>{name}</h2>
<p>Employee ID: {id}</p>
</div>
);
}
React useState
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState<number>(0);
return (
<button onClick={() => setCount(value => value + 1)}>
Count: {count}
</button>
);
}
React Event Typing
import type { ChangeEvent } from 'react';
function SearchBox() {
const handleChange = (
event: ChangeEvent<HTMLInputElement>
): void => {
console.log(event.target.value);
};
return (
<input onChange={handleChange} />
);
}
React API Model
interface Employee {
id: number;
name: string;
department: string;
}
interface ApiResponse<T> {
success: boolean;
data: T;
}
TypeScript helps React developers define component props, state, event types, API models, and reusable hooks.
TypeScript with ASP.NET Core Web API
One of the most useful applications of TypeScript for a .NET developer is defining frontend models that correspond to backend API contracts.
ASP.NET Core DTO
public class EmployeeDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
}
Equivalent TypeScript Interface
export interface EmployeeDto {
id: number;
name: string;
department: string;
}
The frontend interface describes the expected JSON shape returned by the backend.
ASP.NET Core API Endpoint
[HttpGet]
public IActionResult GetEmployees()
{
var employees = new List<EmployeeDto>
{
new EmployeeDto
{
Id = 1,
Name = "Alex",
Department = "IT"
}
};
return Ok(employees);
}
Frontend API Service
async function getEmployees(): Promise<EmployeeDto[]> {
const response = await fetch(
"https://localhost:7001/api/employees"
);
if (!response.ok) {
throw new Error("API request failed");
}
return response.json() as Promise<EmployeeDto[]>;
}
Understanding the Request Flow
Angular / React Application
|
| HTTP Request
v
ASP.NET Core Web API
|
v
Controller / Minimal API
|
v
Business Logic / Service Layer
|
v
Entity Framework Core / Dapper
|
v
SQL Server
|
v
JSON Response
|
v
TypeScript Frontend Model
Important Integration Concepts
- Frontend TypeScript interfaces are separate from backend C# DTO classes.
- Matching names and properties do not automatically synchronize the two contracts.
- JSON property naming policies must be considered.
- Nullability should be handled consistently between backend and frontend.
- Use API contract generation or shared schema tooling when stronger synchronization is required.
- Use runtime validation when API data must be checked before application logic relies on it.
API Models, DTOs and Type Safety
TypeScript interfaces and types can describe API request and response contracts.
Request Model
interface CreateEmployeeRequest {
name: string;
department: string;
email: string;
}
Response Model
interface EmployeeResponse {
id: number;
name: string;
department: string;
email: string;
}
Update Model
type UpdateEmployeeRequest =
Partial<CreateEmployeeRequest>;
Use Partial only when the API actually accepts optional fields. It should not be used as a substitute for understanding the backend contract.
API Response Wrapper
interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
Typed API Method
async function createEmployee(
request: CreateEmployeeRequest
): Promise<EmployeeResponse> {
const response = await fetch("/api/employees", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error("Unable to create employee");
}
return response.json() as Promise<EmployeeResponse>;
}
Validation and Runtime Type Safety
TypeScript checks types during development, but it cannot guarantee that external data matches an interface when the application runs.
For example, a backend response may contain unexpected fields, missing properties, or values of the wrong type.
Why Runtime Validation Matters
interface Employee {
id: number;
name: string;
}
// This assertion does not validate the JSON.
const employee = JSON.parse(jsonText) as Employee;
The assertion only tells the compiler how to treat the value.
Runtime Validation with Zod
Zod is an external schema-validation library that can validate unknown data at runtime and infer TypeScript types from schemas.
import { z } from "zod";
const EmployeeSchema = z.object({
id: z.number(),
name: z.string(),
department: z.string()
});
type Employee = z.infer<typeof EmployeeSchema>;
const unknownData: unknown = {
id: 101,
name: "Alex",
department: "IT"
};
const employee = EmployeeSchema.parse(unknownData);
safeParse
const result = EmployeeSchema.safeParse(unknownData);
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.issues);
}
Runtime validation is especially useful for API responses, form inputs, environment variables, and external integrations.
Testing and Debugging
Testing verifies that application logic behaves as expected. TypeScript does not replace unit testing or integration testing.
Unit Testing Example
export function add(a: number, b: number): number {
return a + b;
}
Example test using Vitest:
import { describe, it, expect } from "vitest";
import { add } from "./math";
describe("add", () => {
it("should return the sum", () => {
expect(add(10, 20)).toBe(30);
});
});
Common Testing Tools
- Vitest
- Jest
- Playwright
- Cypress
- Testing Library
Debugging
- Use source maps for debugging generated JavaScript.
- Use browser developer tools for frontend applications.
- Use breakpoints in Visual Studio Code.
- Check TypeScript compiler errors.
- Inspect network requests and API responses.
Build, Deployment and Best Practices
Build Process
A typical TypeScript application build includes:
- Writing TypeScript source files.
- Type checking.
- Transforming TypeScript and JSX where applicable.
- Bundling modules and application assets.
- Generating production-ready JavaScript and static assets.
- Deploying the output to a hosting environment.
Common Build Tools
| Tool | Purpose |
|---|---|
| TypeScript Compiler (tsc) | Type checking and JavaScript output. |
| Vite | Modern frontend development and build tooling. |
| Angular CLI | Angular application development and build tooling. |
| Webpack | Module bundling and asset processing. |
| esbuild | Fast JavaScript and TypeScript transformation and bundling. |
| SWC | Fast compiler and transformation tooling. |
Best Practices
- Enable strict type checking for new projects.
- Prefer
unknownoveranywhen the type is not known. - Use interfaces or type aliases to define clear data contracts.
- Use generics to build reusable and type-safe utilities.
- Use type inference where the type is obvious.
- Keep components, models, services, and utilities organized.
- Avoid unnecessary type assertions.
- Handle null and undefined explicitly.
- Use runtime validation for untrusted data.
- Keep frontend API models aligned with backend DTOs.
- Use consistent naming conventions.
- Keep dependencies and compiler versions compatible.
- Write unit tests for important business logic.
Common TypeScript Mistakes
| Mistake | Better approach |
|---|---|
| Using any everywhere | Prefer specific types or unknown. |
| Ignoring null and undefined | Use strict null checks and explicit handling. |
| Using type assertions as validation | Validate external data at runtime. |
| Overcomplicating types | Prefer readable, maintainable type definitions. |
| Duplicating API contracts without control | Use generated contracts or a documented synchronization process. |
| Using var in modern code | Prefer const and let. |
| Confusing interface with runtime validation | Remember interfaces are erased from JavaScript output. |
| Ignoring compiler errors | Fix errors and review compiler configuration. |
Practical Project: Employee Management Frontend
Now let's combine the concepts in a practical application scenario.
Project: Employee Management Frontend using TypeScript and ASP.NET Core Web API.
Project Requirements
- Display all employees.
- Retrieve an employee by ID.
- Create a new employee.
- Update employee information.
- Delete an employee.
- Handle API errors.
- Use typed request and response models.
Step 1: Define the Employee Model
export interface Employee {
id: number;
name: string;
email: string;
department: string;
salary: number;
}
Step 2: Define the Create Request
export interface CreateEmployeeRequest {
name: string;
email: string;
department: string;
salary: number;
}
Step 3: Create the Service
import type {
Employee,
CreateEmployeeRequest
} from "./employee.model";
const API_URL = "https://localhost:7001/api/employees";
export class EmployeeService {
async getAll(): Promise<Employee[]> {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error("Unable to retrieve employees");
}
return response.json() as Promise<Employee[]>;
}
async getById(id: number): Promise<Employee> {
const response = await fetch(`${API_URL}/${id}`);
if (!response.ok) {
throw new Error("Employee not found");
}
return response.json() as Promise<Employee>;
}
async create(
request: CreateEmployeeRequest
): Promise<Employee> {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error("Unable to create employee");
}
return response.json() as Promise<Employee>;
}
async delete(id: number): Promise<void> {
const response = await fetch(`${API_URL}/${id}`, {
method: "DELETE"
});
if (!response.ok) {
throw new Error("Unable to delete employee");
}
}
}
Step 4: Consume the Service
import { EmployeeService } from "./employee.service";
const service = new EmployeeService();
async function loadEmployees(): Promise<void> {
try {
const employees = await service.getAll();
console.log(employees);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
loadEmployees();
TypeScript Learning Roadmap
Follow this learning path to build a strong foundation and progress toward professional development.
Foundation
- JavaScript fundamentals
- TypeScript introduction
- Basic data types
- Variables and type inference
- Arrays and tuples
- Objects and interfaces
- Functions and parameters
Intermediate
- Union and intersection types
- Enums and literal types
- Type narrowing
- Classes and inheritance
- Generics
- Modules and imports
- Promises and async/await
Advanced
- Advanced type manipulation
- Conditional and mapped types
- Utility types
- Decorators
- Strict compiler configuration
- Runtime validation
- Testing and debugging
Professional Application Development
- TypeScript with Angular
- TypeScript with React
- API integration with ASP.NET Core
- Authentication and authorization integration
- Frontend architecture
- Build and deployment
Important TypeScript Interview Questions
- What is TypeScript?
- What is the difference between TypeScript and JavaScript?
- What is static type checking?
- What is type inference?
- What is the difference between any and unknown?
- What is the difference between interface and type?
- What are union and intersection types?
- What are generics?
- What is the difference between null and undefined?
- What is type narrowing?
- What are type guards?
- What is the keyof operator?
- What is the typeof operator in type positions?
- What are mapped types?
- What are conditional types?
- What is the satisfies operator?
- What are utility types?
- What is the difference between type assertion and type casting?
- What is tsconfig.json?
- What is the difference between private and protected?
- What are decorators?
- What is the difference between Promise.all and Promise.allSettled?
- How does TypeScript integrate with React?
- How does Angular use TypeScript?
- How do you integrate TypeScript with ASP.NET Core Web API?
- Why is runtime validation required?
Official Learning Resources
Use official documentation to explore language features, compiler behavior, and framework integration.
- TypeScript Official Documentation
- TypeScript Handbook
- Everyday Types
- Generics
- Utility Types
- TypeScript 7.0 Release Notes
- Official TypeScript Blog
- Angular Documentation
- React Documentation
What’s Next?
After learning TypeScript, the next step is to apply it in real-world frontend development.
Practice building applications using Angular or React, integrate them with ASP.NET Core Web APIs, define reusable models and services, handle API responses, and implement proper validation and error handling.
Focus on writing clean, maintainable, type-safe code rather than simply learning syntax. Understanding how TypeScript works with JavaScript, modern frontend frameworks, and backend API contracts will help you build scalable full-stack applications.