TypeScript Essentials for Angular
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · TypeScript Essentials for Angular Companion Article
Angular applications are primarily written in TypeScript. Although JavaScript knowledge is useful, understanding TypeScript makes Angular code easier to read, maintain, refactor, and debug. TypeScript adds features such as static typing, interfaces, generics, access modifiers, decorators, and compile-time checking on top of JavaScript. Angular uses many of these features throughout components, services, dependency injection, routing, forms, and HTTP communication. This chapter covers the TypeScript concepts that are most useful when working with Angular applications.
TypeScript is a programming language built on top of JavaScript. Valid JavaScript is generally valid TypeScript, but TypeScript provides additional language features that help developers catch mistakes before the application runs.
For example, JavaScript allows a variable to hold values of completely different types:
let age = 30;
age = "Thirty";
TypeScript can prevent this kind of accidental assignment when the variable is expected to contain a number.
let age: number = 30;
// Error
age = "Thirty";
In Angular projects, TypeScript helps with:
Variables store values used by an application.
Modern TypeScript mainly uses let and const.
let username: string = "Rahul";
let age: number = 28;
const country: string = "India";
Use let when the value may change.
let score: number = 10;
score = 20;
Use const when the variable should not be reassigned.
const applicationName = "Employee Portal";
Stores text.
let firstName: string = "Amit";
Stores integers and decimal numbers.
let salary: number = 75000;
let rating: number = 4.5;
Stores true or false.
let isLoggedIn: boolean = true;
Represents an explicitly empty value.
let selectedUser: null = null;
Represents a value that has not been assigned.
let response: undefined;
Allows almost any type of value.
let data: any = "Angular";
data = 100;
data = true;
any removes much of TypeScript's type checking, so it should normally be avoided when the correct type can be defined.
unknown can store different types while requiring type checking before the value is used.
let response: unknown = "Success";
if (typeof response === "string") {
console.log(response.toUpperCase());
}
unknown is usually safer than any when the type of external data is uncertain.
TypeScript does not always require an explicit type declaration.
It can often determine the type automatically from the assigned value.
let framework = "Angular";
TypeScript infers that framework is a string.
The following assignment would therefore produce an error:
framework = 100;
Another example:
let count = 10;
TypeScript infers:
number
Type inference reduces unnecessary code while preserving type safety.
Both of the following are valid:
let city: string = "Pune";
let city = "Pune";
Explicit types are particularly useful when the intended type is not obvious or when defining APIs, models, parameters, and return values.
Functions contain reusable logic.
A TypeScript function can specify types for both its parameters and return value.
function add(a: number, b: number): number {
return a + b;
}
Calling the function:
const result = add(10, 20);
console.log(result);
function getWelcomeMessage(name: string): string {
return `Welcome ${name}`;
}
Use void when a function does not return a meaningful value.
function printMessage(message: string): void {
console.log(message);
}
Arrow functions are commonly used in Angular applications.
const multiply = (a: number, b: number): number => {
return a * b;
};
Short form:
const square = (number: number): number => number * number;
Arrow functions frequently appear in array operations, subscriptions, transformations, and callbacks.
Example:
users.filter(user => user.active);
A parameter can be made optional using ?.
function greet(name?: string): string {
return name ? `Hello ${name}` : "Hello";
}
Both calls are valid:
greet();
greet("Neha");
A function parameter can have a default value.
function calculatePrice(price: number, tax: number = 18): number {
return price + (price * tax / 100);
}
Calling:
calculatePrice(1000);
Here, tax automatically becomes 18.
Arrays store multiple values.
An array of numbers:
let marks: number[] = [70, 80, 90];
An array of strings:
let technologies: string[] = [
"Angular",
"TypeScript",
"Java"
];
Alternative generic syntax:
let technologies: Array<string> = [
"Angular",
"TypeScript"
];
Both forms are valid.
let numbers: number[] = [10, 20, 30];
numbers.push(40);
numbers.pop();
Angular applications commonly use methods such as:
map()
filter()
find()
some()
every()
reduce()
Example:
const activeUsers = users.filter(user => user.active);
Objects store related values using properties.
const employee = {
id: 101,
name: "Rahul",
department: "IT"
};
An explicit object type can also be declared.
let employee: {
id: number;
name: string;
department: string;
};
employee = {
id: 101,
name: "Rahul",
department: "IT"
};
For reusable object structures, interfaces or type aliases are usually more convenient.
An interface describes the expected structure of an object.
interface User {
id: number;
name: string;
email: string;
}
Creating an object:
const user: User = {
id: 1,
name: "Amit",
email: "amit@example.com"
};
If a required property is missing, TypeScript can report an error.
const user: User = {
id: 1,
name: "Amit"
};
The object does not satisfy the User interface because email is missing.
Interfaces are frequently used to represent API data.
export interface Product {
id: number;
name: string;
price: number;
available: boolean;
}
A service can then return typed data.
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
This provides stronger type checking when working with HTTP responses.
A type alias creates a reusable name for a type.
type UserId = number;
Usage:
let currentUserId: UserId = 101;
Type aliases can also represent object structures.
type Employee = {
id: number;
name: string;
salary: number;
};
Example:
const employee: Employee = {
id: 1,
name: "Kiran",
salary: 60000
};
Interfaces and type aliases can both describe object structures.
Example using an interface:
interface Customer {
id: number;
name: string;
}
Example using a type:
type Customer = {
id: number;
name: string;
};
Interfaces are particularly convenient for object-oriented structures and extension.
Type aliases are flexible and can represent unions, primitives, tuples, and other combinations.
For example:
type Status = "pending" | "approved" | "rejected";
A class is a blueprint for creating objects.
class Employee {
name: string;
salary: number;
constructor(name: string, salary: number) {
this.name = name;
this.salary = salary;
}
displayDetails(): void {
console.log(this.name, this.salary);
}
}
Creating an object:
const employee = new Employee("Rahul", 70000);
employee.displayDetails();
Angular heavily uses classes for concepts such as components and services.
A simplified Angular component is a class:
export class UserComponent {
title = "Users";
}
Access modifiers control where class members can be accessed.
TypeScript supports commonly used modifiers such as:
publicprivateprotectedPublic members can be accessed from outside the class.
class User {
public name = "Rahul";
}
const user = new User();
console.log(user.name);
public is the default visibility when no access modifier is specified.
Private members can only be accessed inside the class.
class BankAccount {
private balance = 5000;
getBalance(): number {
return this.balance;
}
}
The following direct access is not allowed:
account.balance;
Protected members are accessible inside the class and its subclasses.
class Employee {
protected salary = 50000;
}
class Manager extends Employee {
showSalary(): void {
console.log(this.salary);
}
}
A constructor runs when an object is created from a class.
class Product {
name: string;
constructor(name: string) {
this.name = name;
}
}
Usage:
const product = new Product("Laptop");
TypeScript allows shorter class declarations.
Instead of:
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
}
You can write:
class Product {
constructor(
public name: string,
public price: number
) {}
}
TypeScript automatically creates the properties.
This syntax is commonly encountered in Angular code.
Angular often uses constructors to receive dependencies.
Example:
constructor(private userService: UserService) {}
Angular's dependency injection system supplies the required service instance.
The private keyword also creates a class property named userService.
The component can then use it:
this.userService.getUsers();
Modern Angular also supports the inject() API, so constructor injection is not the only dependency-injection style you may encounter.
Inheritance allows one class to reuse or extend another class.
class Person {
constructor(public name: string) {}
introduce(): void {
console.log(`My name is ${this.name}`);
}
}
Child class:
class Employee extends Person {
constructor(
name: string,
public employeeId: number
) {
super(name);
}
}
Creating an object:
const employee = new Employee("Amit", 101);
employee.introduce();
The extends keyword creates inheritance.
The super() call invokes the parent constructor.
Inheritance can be useful, but Angular applications often favor composition and reusable services over creating deep inheritance hierarchies.
Generics allow reusable code to work with different data types while maintaining type safety.
Without generics, developers may be tempted to use any.
function getValue(value: any): any {
return value;
}
A generic version is safer:
function getValue<T>(value: T): T {
return value;
}
Usage:
const name = getValue<string>("Angular");
const age = getValue<number>(30);
Here, T represents a type supplied when the function is used.
interface ApiResponse<T> {
success: boolean;
data: T;
}
A user response:
interface User {
id: number;
name: string;
}
const response: ApiResponse<User> = {
success: true,
data: {
id: 1,
name: "Rahul"
}
};
A product response could use the same generic structure.
const response: ApiResponse<Product[]> = {
success: true,
data: products
};
Generics are heavily used by Angular and RxJS.
Examples include:
Observable<User>
Observable<User[]>
FormControl<string>
HttpResponse<Product>
An enum provides a group of named values.
enum UserRole {
Admin,
Manager,
Employee
}
Usage:
let role: UserRole = UserRole.Admin;
String enums can make stored values more readable.
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
Usage:
const status: OrderStatus = OrderStatus.Pending;
For many Angular applications, literal union types are also a useful alternative when only a small set of string values is required.
Example:
type OrderStatus = "pending" | "processing" | "completed";
A union allows a value to belong to more than one type.
let userId: string | number;
Both are valid:
userId = 101;
userId = "USR101";
Union types are particularly useful when an application accepts a limited group of possible values.
type ButtonSize = "small" | "medium" | "large";
Usage:
let size: ButtonSize = "medium";
The following would be invalid:
size = "huge";
Before performing type-specific operations, check the actual type.
function printId(id: string | number): void {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
This process is called type narrowing.
An interface property can be optional.
Use ? after the property name.
interface User {
id: number;
name: string;
phone?: string;
}
Both objects are valid:
const user1: User = {
id: 1,
name: "Rahul"
};
const user2: User = {
id: 2,
name: "Amit",
phone: "9876543210"
};
Optional properties are useful when API data does not always contain every field.
Optional chaining helps safely access nested values that may be null or undefined.
const city = user.address?.city;
If address is unavailable, JavaScript does not attempt to access city and the expression evaluates to undefined.
Without optional chaining, additional checks may be needed.
if (user.address) {
console.log(user.address.city);
}
Optional chaining is widely used when handling API data and optional application state.
The nullish coalescing operator ?? provides a fallback when a value is null or undefined.
const username = user.name ?? "Guest";
If user.name is null or undefined, "Guest" is used.
This differs from ||, which also treats values such as 0, false, and an empty string as falsy.
Modules help divide an application into reusable files.
A value must normally be exported before another file can import it.
export interface User {
id: number;
name: string;
}
import { User } from "./user";
Functions can also be exported.
export function calculateTax(amount: number): number {
return amount * 0.18;
}
Then imported:
import { calculateTax } from "./tax-utils";
Angular projects rely heavily on ES module import and export syntax.
For example:
import { Component } from "@angular/core";
When an import is required only for TypeScript type checking, it can be declared as a type import.
import type { User } from "./user";
This makes the intention clear that User is being used only as a type.
Example:
import type { User } from "./user";
function displayUser(user: User): void {
console.log(user.name);
}
A decorator adds metadata or behavior to a class or class member.
Angular uses decorators extensively.
A common example is @Component.
import { Component } from "@angular/core";
@Component({
selector: "app-user",
templateUrl: "./user.component.html"
})
export class UserComponent {}
The @Component decorator tells Angular that the class represents a component and supplies Angular with its configuration.
Other Angular decorators you may encounter include:
@Injectable
@Input
@Output
@Directive
@Pipe
For example:
@Injectable({
providedIn: "root"
})
export class UserService {}
@Injectable makes the class available to Angular's dependency-injection system according to its provider configuration.
Angular developers generally consume Angular-provided decorators rather than creating decorators for routine application development.
Web applications frequently perform operations that do not complete immediately.
Examples include:
JavaScript and TypeScript support promises and async/await for asynchronous programming.
A promise represents a value that may become available later.
function getUsername(): Promise<string> {
return Promise.resolve("Rahul");
}
The result can be handled using:
getUsername().then(name => {
console.log(name);
});
async and await provide a more sequential syntax for working with promises.
async function loadUser(): Promise<void> {
const user = await getUser();
console.log(user);
}
An async function always returns a promise.
For example:
async function getNumber(): Promise<number> {
return 10;
}
Although the function appears to return 10, callers receive a Promise<number>.
Use try, catch, and optionally finally.
async function loadData(): Promise<void> {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error("Unable to load data", error);
} finally {
console.log("Request finished");
}
}
This structure is useful when working with promise-based APIs.
Angular applications frequently use RxJS Observables, especially for Angular APIs such as HttpClient.
Example:
this.http.get<User[]>("/api/users");
This returns an Observable<User[]>, not a promise.
A component might subscribe to it:
this.userService.getUsers().subscribe(users => {
this.users = users;
});
Therefore, async/await does not replace Observables in Angular.
A useful distinction is:
| Feature | Promise | Observable |
|---|---|---|
| Typical result | One asynchronous result | Zero, one, or many emitted values |
| Lazy behavior | Promise work usually starts when created | Observable execution commonly starts on subscription |
| Operators | Limited built-in transformation API | Rich RxJS operator ecosystem |
| Cancellation | No general built-in cancellation model for arbitrary promises | Subscriptions can be unsubscribed |
| Common Angular use | Promise-based APIs | HTTP, forms, routing and reactive streams |
Angular developers should understand both approaches.
One of the most useful TypeScript practices in Angular is defining types for backend responses.
Suppose an API returns:
{
"id": 101,
"name": "Laptop",
"price": 45000
}
Create an interface:
export interface Product {
id: number;
name: string;
price: number;
}
Then use the interface in the service.
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(`${this.apiUrl}/${id}`);
}
For multiple products:
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(this.apiUrl);
}
This improves autocompletion and compile-time checking throughout the application.
However, TypeScript types do not validate server data at runtime. If runtime validation is required, the application must perform that validation separately.
Consider a user-management application.
export interface User {
id: number;
name: string;
email: string;
role: UserRole;
phone?: string;
}
export enum UserRole {
Admin = "ADMIN",
Manager = "MANAGER",
Employee = "EMPLOYEE"
}
export interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
The response can then be typed as:
ApiResponse<User>
or:
ApiResponse<User[]>
A service method might look like:
getUsers(): Observable<ApiResponse<User[]>> {
return this.http.get<ApiResponse<User[]>>(this.apiUrl);
}
This example combines several TypeScript concepts:
Sometimes the developer knows more about a value's type than TypeScript can determine.
A type assertion can be used.
const input = document.getElementById("username") as HTMLInputElement;
Now TypeScript understands that input is an HTMLInputElement.
You can access properties such as:
input.value;
Type assertions do not convert the actual runtime value. They only tell TypeScript how the developer wants the value to be treated.
Incorrect assertions can therefore hide bugs.
readonly prevents a property from being reassigned through normal TypeScript usage after initialization.
interface Configuration {
readonly apiUrl: string;
}
Example:
const config: Configuration = {
apiUrl: "/api"
};
The following reassignment is rejected by TypeScript:
config.apiUrl = "/new-api";
readonly is useful for values that should remain stable after initialization.
Literal types restrict a variable to exact values.
let direction: "left" | "right";
Valid:
direction = "left";
direction = "right";
Invalid:
direction = "up";
Literal types are useful for configuration options, component states, and other finite sets of values.
A tuple represents an array with known positions and types.
let user: [number, string];
user = [101, "Rahul"];
The first value must be a number and the second must be a string.
Tuples can be useful when positional data has a clearly defined structure, although named object properties are often easier to understand in application code.
never Typenever represents a situation where a function cannot complete normally.
Example:
function throwError(message: string): never {
throw new Error(message);
}
Because the function always throws an exception, it never returns a normal value.
never also appears in advanced type checking when TypeScript determines that no possible type remains.
void Typevoid is commonly used when a function performs an operation but does not return a useful value.
function logMessage(message: string): void {
console.log(message);
}
Angular event-handling methods frequently return void.
onSave(): void {
console.log("Saved");
}
any EverywhereAvoid:
let user: any;
Prefer:
let user: User;
Proper typing helps TypeScript find errors earlier.
This:
this.http.get<User>(url);
tells TypeScript how you expect to treat the response.
It does not guarantee that the backend actually returned a valid User.
Runtime validation may still be necessary for untrusted or externally controlled data.
Code such as:
console.log(user.address.city);
may fail if address is unavailable.
When appropriate, use optional chaining:
console.log(user.address?.city);
The ! operator tells TypeScript that a value will not be null or undefined.
user!.name;
This should be used carefully.
If the assumption is incorrect, the application can still fail at runtime.
Inheritance can be useful, but Angular applications usually benefit more from:
Avoid introducing inheritance when a simpler design solves the problem.
Type inference is useful, but explicit return types can make important methods easier to understand.
For example:
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl);
}
The method contract is immediately visible.
A typical Angular class can combine many TypeScript concepts.
export class ProductListComponent {
products: Product[] = [];
selectedProduct?: Product;
loading = false;
constructor(private productService: ProductService) {}
loadProducts(): void {
this.loading = true;
this.productService.getProducts().subscribe({
next: products => {
this.products = products;
},
error: error => {
console.error(error);
},
complete: () => {
this.loading = false;
}
});
}
}
This example contains:
void return typeUnderstanding these TypeScript concepts makes Angular component code considerably easier to follow.
A service commonly contains typed methods and generic APIs.
export class ProductService {
constructor(private http: HttpClient) {}
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>("/api/products");
}
getProduct(id: number): Observable<Product> {
return this.http.get<Product>(`/api/products/${id}`);
}
}
Important TypeScript concepts involved here include:
Use the most precise type that accurately describes the data.
Instead of:
let status: string;
when only three values are allowed, consider:
type Status = "pending" | "approved" | "rejected";
let status: Status;
Instead of:
let products: any[];
prefer:
let products: Product[];
Instead of:
let response: any;
consider defining:
interface ApiResponse<T> {
data: T;
message: string;
}
Specific types make code easier to understand and help the compiler identify incorrect assignments.
| Requirement | TypeScript Feature |
|---|---|
| Store text | string |
| Store numeric values | number |
| Store true/false | boolean |
| Store multiple values | Array |
| Describe an object | interface or type |
| Allow multiple possible types | Union |
| Make a property optional | ? |
| Reuse logic | Function |
| Model objects | Class |
| Control member access | Access modifiers |
| Share code between files | Modules |
| Create reusable typed structures | Generics |
| Define named fixed values | Enum |
| Add Angular metadata | Decorators |
| Work with promises | async / await |
| Safely access optional values | ?. |
| Provide null/undefined fallback | ?? |
| Prevent reassignment of a property | readonly |
You do not need to master every advanced feature of TypeScript before learning Angular. However, you should be comfortable with:
These concepts appear repeatedly in real Angular projects.
Consider an Angular application that displays courses.
Create the following interface:
interface Course {
id: number;
title: string;
price: number;
category?: string;
}
Create an array:
const courses: Course[] = [
{
id: 1,
title: "Angular",
price: 999
},
{
id: 2,
title: "TypeScript",
price: 699,
category: "Programming"
}
];
Filter courses:
const affordableCourses = courses.filter(
course => course.price < 800
);
Create a generic response:
interface ApiResponse<T> {
data: T;
success: boolean;
}
Use it with the courses:
const response: ApiResponse<Course[]> = {
data: courses,
success: true
};
This small example practices interfaces, arrays, objects, optional properties, arrow functions, generics, and type inference together.
TypeScript is not simply JavaScript with extra syntax. In Angular development, it acts as a type system that helps describe how application data, classes, functions, services, and APIs should behave.
The most useful habits are to:
anynull and undefined deliberatelyOnce these TypeScript fundamentals are comfortable, Angular concepts such as components, services, dependency injection, forms, routing, and HTTP communication become much easier to understand.