So you've:
You should be ready to land that job right... ❓
Maybe!
But here’s the thing. Some interviewers may ask you some more specific questions about TypeScript before they get into the actual coding and technical problem-solving.
In theory, you should know the answers to all these questions, but it never hurts to be prepared.
This is why in this guide, I’m going to walk you through 42 TypeScript focused questions, along with their answers and code examples.
So grab a coffee, read along, and see how many of these you can answer correctly.
Sidenote: If you find that you’re struggling with the questions in this guide, or just want to dive deeper into TypeScript and build some more impressive projects for your portfolio, then come and check out my complete TypeScript Developer course.
With that out of the way, let’s get into the questions.
TypeScript is a statically typed superset of JavaScript that compiles down to plain JavaScript.
It was developed by Microsoft to help deal with the complexities of large-scale applications and achieves this by adding static types to JavaScript, providing enhanced tooling like autocompletion, navigation, and refactoring capabilities, which makes it easier to read and debug code.
Key differences between TypeScript and JavaScript include:
There are three main components in TypeScript:
tsc
) is responsible for transpiling TypeScript code (.ts
files) into JavaScript (.js
files). It catches errors at compile time and enforces type-checkingWe’ve hinted at a few already, but let’s break them down.
Often, the benefits of error checking, autocompletion, and better navigation make TypeScript worth the overhead for larger projects, but it may not be worth it for small, rapid prototypes.
In TypeScript, just like in JavaScript, a variable is a symbolic name for a value. (Variables are fundamental in programming because they allow us to store and manipulate data in our programs).
You can declare a variable in TypeScript using the let
or const
keywords, just like in modern JavaScript. However, TypeScript introduces type annotations that enable you to explicitly specify the type of data a variable can hold:
let isDone: boolean = false;
let lines: number = 42;
let greetings: string = "Hello, World!";
In this example, isDone
is a boolean variable, lines
is a number variable, and greetings
is a string variable.
This means that if you try to assign a value of a different type to these variables, TypeScript will throw an error at compile time, like so:
isDone = 1; // Error: Type 'number' is not assignable to type 'boolean'.
lines = "forty-two"; // Error: Type 'string' is not assignable to type 'number'.
greetings = true; // Error: Type 'boolean' is not assignable to type 'string'.
This is actually a good thing.
By using type annotations like this, TypeScript can help to catch errors early, making your code more robust and maintainable.
Interfaces provide a way to specify the structure and behaviors of any given type in TypeScript. They act as a contract in your code by defining the properties and methods that an object must implement.
This ensures that the object adheres to a specific structure, making your code more predictable and easier to debug.
For example
interface Greeting {
message: string;
}
function greet(g: Greeting) {
console.log(g.message);
}
greet({ message: 'Hello, TypeScript!' });
In this example:
Greeting
is an interface that defines a contract for objects, specifying that any object of type Greeting
must have a property message
of type string
greet
takes an argument g
of type Greeting
. Since g
must adhere to the Greeting
interface, it guarantees that g
will have a message
property that is a stringgreet
with an object { message: 'Hello, TypeScript!' }
, TypeScript ensures that this object meets the Greeting
interface's contractBecause the message
property is guaranteed to exist by the TypeScript compiler, there’s no need to check for null
or undefined
before accessing it. This makes your code safer and reduces the likelihood of runtime errors.
Classes are a fundamental component of object-oriented programming (OOP), and TypeScript includes them as a part of the language. They provide a great deal of flexibility in structuring your code. Classes in TypeScript support features such as inheritance, interfaces, access modifiers, and more.
Here’s an example of a simple class:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
console.log(`Hello, ${this.greeting}`);
}
}
let greeter = new Greeter('TypeScript');
greeter.greet(); // Outputs: Hello, TypeScript
In this example, Greeter
is a class with three members: a property called greeting
, a constructor, and a method greet
. You use the new
keyword to create instances of a class.
Classes can also implement interfaces, which allow you to enforce that a class adheres to a specific contract.
For example:
interface Animal {
name: string;
speak(): void;
}
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const myDog = new Dog('Rex');
myDog.speak(); // Outputs: Rex barks.
In this example:
Animal
is an interface with a property name
of type string
and a method speak
Dog
implements the Animal
interface, providing a concrete implementation of the speak
methodDog
adheres to the Animal
interface, guaranteeing that all instances of Dog
have a name
property and a speak
methodBy using classes and interfaces together, you can create flexible, reusable, and well-structured code. Classes in TypeScript also support features like inheritance and access modifiers (public
, private
, protected
), which provide further control over how your code is organized and accessed.
Yes. TypeScript is a superset of JavaScript which means you can use static types as well as dynamic types. This flexibility means you can have strongly typed variables, function parameters, and objects.
However, you can also use the any
type to opt out of compile-time type checking if you want to leverage JavaScript's dynamic typing.
Here's an example in TypeScript:
let foo: any = 42; // OK
foo = 'Hello'; // OK
foo = true; // OK
In the above example, foo
is declared with the type any
, allowing it to hold any type of value.
But when the type is specified:
let foo: number = 42; // OK
foo = 'Hello'; // Error: Type 'string' is not assignable to type 'number'.
foo = true; // Error: Type 'boolean' is not assignable to type 'number'.
In TypeScript, just as in ECMAScript 2015, a module is a file containing variables, functions, classes, etc. The key difference is that these variables, functions, classes, etc., are private by default in TypeScript, and you decide what to expose by exporting them.
You can import functionality from other modules using the import
keyword.
For example:
math.ts:
export function add(x: number, y: number): number {
return x + y;
}
app.ts:
import { add } from './math';
console.log(add(4, 5)); // Outputs: 9
In the example above, the math
module exports a function named add
. The app
module can then import this function and use it.
These concepts are key to building scalable applications and fostering code reuse.
Decorators are a design pattern in JavaScript, and they're available in TypeScript as well. They're a way to modify classes or properties at design time.
TypeScript Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. They can be used to modify classes, methods, and properties.
Here's an example of a class decorator:
function log(target: Function) {
console.log(`${target.name} - has been logged`);
}
@log
class MyClass {}
In this example, log
is a decorator that logs the name of the class or function. The @log
syntax is then used to apply the decorator to a class.
null
and undefined
?In JavaScript, null
and undefined
are common sources of bugs and confusion.
However, TypeScript addresses this with features like strict null checks and nullable types to help catch these issues before they cause problems.
--strictNullChecks
)When strict null checks are enabled, TypeScript ensures that you check whether an object is defined before accessing its properties. This helps prevent runtime errors caused by null
or undefined
values.
For example:
Without strict null checks, assigning null
or undefined
to a variable that expects a number does not produce an error:
let x: number;
x = null; // No error without strict null checks
x = undefined; // No error without strict null checks
But, with strict null checks enabled, the above code would produce errors:
let x: number;
x = null; // Error: Type 'null' is not assignable to type 'number'.
x = undefined; // Error: Type 'undefined' is not assignable to type 'number'.
So, if you want a variable to accept null
or undefined
, you can use a union type.
If you want a variable to accept null
or undefined
, you can use a union type to explicitly allow these values.
For example:
let x: number | null | undefined;
x = null; // Ok
x = undefined; // Ok
In this example, x
can be a number
, null
, or undefined
. This explicit typing makes it clear that x
can hold any of these values, reducing the chance of unexpected errors in your code.
Generics are a tool for creating reusable code. They allow a type to be specified dynamically, providing a way to create reusable and general-purpose functions, classes, and interfaces without sacrificing type safety.
Here's an example of a generic function that returns whatever is passed into it:
function identity<T>(arg: T): T {
return arg;
}
console.log(identity("myString")); // Output: myString
In this example, T
acts as a placeholder for any type. The type is typically inferred automatically by TypeScript, but the can specify the exact T
when calling the function.
Generics are useful because they enable you to write flexible and reusable code while maintaining type safety.
In TypeScript, enums are a way of creating named constants, making code more readable and less error-prone.
Here's how you can define and use an enum:
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
let day: Weekday;
day = Weekday.Monday;
In the example above, Weekday
is an enum type that can contain one of the listed seven days. Enums can improve your code by enforcing that variables take one of a few predefined constants.
In TypeScript, each class member is public by default, which means they can be accessed anywhere without any restrictions.
However, TypeScript also supports access modifiers similar to many object-oriented languages such as Java and C#. Access modifiers control the visibility and accessibility of the class members (properties, methods, etc.).
There are three types of access modifiers in TypeScript: public
, private
, and protected
.
Public members are visible everywhere, and it's the default access level for class members.
class Car {
public color: string;
public constructor(color: string) {
this.color = color;
}
}
In this example, both the color
property and the constructor
method are public and can be accessed outside of the Car
class.
Private members are only accessible within the class that declares them.
class Car {
private color: string;
public constructor(color: string) {
this.color = color;
}
}
Here, the color
property is private and cannot be accessed outside of the Car
class.
Protected members are like private members,but can also be accessed within subclasses.
class Vehicle {
protected color: string;
constructor(color: string) {
this.color = color;
}
}
class Car extends Vehicle {
constructor(color: string) {
super(color);
console.log(this.color); // Accessible because `color` is protected
}
}
In this example, color
is a protected property. It's not accessible outside of the Vehicle
class, but it is accessible within the Car
class because Car
extends Vehicle
.
Namespaces in TypeScript are used to group related code together, which helps in organizing and structuring your code. They also help prevent naming collisions by creating a scope for identifiers such as variables, functions, classes, and interfaces. For example:
namespace MyNamespace {
export class MyClass {
public static doSomething() {
console.log("Doing something...");
}
}
}
MyNamespace.MyClass.doSomething(); // Outputs: "Doing something..."
In this example:
MyNamespace
is a namespace that groups the MyClass
class.doSomething
method is defined within MyClass
.MyNamespace.MyClass
, we can access the doSomething
method, avoiding potential naming conflicts with other parts of the code.Important: While namespaces are useful, ES modules have become the preferred way to organize code in modern JavaScript and TypeScript projects.
ES modules offer better support for code splitting, lazy loading, and tree shaking, making them a more powerful and flexible solution.
For example:
Here’s how you would use ES modules to achieve similar functionality:
myModule.ts:
export class MyClass {
public static doSomething() {
console.log("Doing something...");
}
}
main.ts:
import { MyClass } from './myModule';
MyClass.doSomething(); // Outputs: "Doing something..."
In this example:
MyClass
is exported from myModule.ts
MyClass
is imported and used in main.ts
Using ES modules is recommended for new projects as they provide better interoperability with modern JavaScript tools and libraries.
In TypeScript, you can annotate a function by specifying the types of its parameters and its return type. This helps catch errors at compile time and makes your code more predictable and easier to understand.
For example:
let add: (x: number, y: number) => number;
add = function(x, y) {
return x + y;
};
console.log(add(2, 3)); // Outputs: 5
In this example:
add
is a function that takes two parameters, x
and y
, both of type number
, and returns a number
.However, if you try to assign a function that doesn't match the type annotation, TypeScript will throw an error like so:
let add: (x: number, y: number) => number;
add = function(x: number, y: string): number { // Error
return x + y;
};
In this invalid example:
y
is of type string
, but the function type annotation specifies that it should be a number
.By using function annotations in TypeScript, you can write more robust and maintainable code, catching potential issues early in the development process.
TypeScript supports asynchronous programming out of the box, using promises and async/await
syntax.
For example:
Here’s how you can use async/await
in TypeScript.
async function fetchUsers() {
const response = await fetch('https://api.github.com/users');
const users = await response.json();
console.log(users);
}
fetchUsers().catch(error => console.log(error));
In this example:
fetchUsers
function is declared as an async
function, which means it returns a promise.await
keyword pauses the function execution until the fetch
promise resolves.await
keyword can only be used inside an async
function.catch
block attached to the fetchUsers
call.Optional chaining is another feature supported by TypeScript. It allows you to access deeply nested properties without worrying if an intermediate property is null
or undefined
.
For example:
type User = {
name: string;
address?: {
street?: string;
city?: string;
}
}
const user: User = {
name: 'John Doe',
};
console.log(user.address?.city); // Outputs: undefined
In this example:
User
is a type with an optional address
property.address
property, if it exists, may contain street
and city
properties.?.
) is used to access user.address.city
.user.address
is undefined
, the expression safely returns undefined
instead of throwing an error.The process is fairly simple, thanks to DefinitelyTyped, which is a large repository of high-quality TypeScript type definitions.
Before using a JavaScript library with TypeScript, you need to install the corresponding type definition package. These packages are available on npm and usually named @types/<library-name>
.
For example:
To use lodash with TypeScript, you would first install lodash and its type definition:
npm install lodash
npm install @types/lodash
Then you can use lodash in your TypeScript code with full type checking:
import _ from 'lodash';
const numbers: number[] = [1, 2, 3, 4];
const reversed = _.reverse(numbers);
console.log(reversed); // Outputs: [4, 3, 2, 1]
Similarly, to use jQuery with TypeScript, you first need to install jQuery and its type definition:
npm install jquery
npm install @types/jquery
Then you can use jQuery in your TypeScript code:
import $ from 'jquery';
$(document).ready(() => {
$('body').text('Hello, world!');
});
In both examples above:
This is one of the key advantages of using TypeScript with popular JavaScript libraries, as it enhances the development experience by making the code more robust and easier to maintain.
Type inference is one of TypeScript's key features, and refers to the automatic detection of the data type of an expression in a programming language. This means that you don't always have to annotate types explicitly.
TypeScript is good at inferring types in many situations. For example, in the declaration and initialization of variables, the return values of functions, and the setting of default parameters.
let x = 10; // `x` is inferred to have the type `number`
In the above example, TypeScript automatically infers the type of x
is number
from the assigned value 10
.
However, TypeScript can't always infer types in all situations.
You'll still want to provide explicit types in many cases, especially when writing function signatures, to avoid accidental incorrect typings and benefit from autocompletion and other type-checking functions.
Type guards are a way to provide additional information about the type of a variable inside a specific block of code. TypeScript supports type guards that let us determine the kind of a variable in conditional branches.
Type guards can be simple typeof
or instanceof
checks, or more advanced user-defined type guards.
For example:
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
console.log(padLeft("Hello, world!", 4)); // Outputs: "Hello, world!"
In this example, typeof padding === "number"
and typeof padding === "string"
are type guards. They provide extra information about the type of padding
within their respective blocks. Inside the if-block where typeof padding === "number"
is true, TypeScript knows that padding
is a number.
Similarly, inside the if-block where typeof padding === "string"
is true, TypeScript knows that padding
is a string.
Type compatibility is based on structural subtyping, which is a form of type checking, and is used to determine the compatibility between types based on their members.
If a type Y has at least the same members as type X, then type Y is said to be compatible with type X.
For example:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
p = new Person(); // OK, because 'Person' has a 'name' member
In this example:
Named
is an interface with one member name
Person
is a class with one member name
TypeScript considers Person
to be compatible with Named
because Person
has at least the same members as Named
. This concept allows for flexible and safe type checking, enabling you to create interchangeable objects that maintain the correct constraints.
Type aliases allow you to create new names for types. They’re kind of similar to interfaces, but they can also name primitives, unions, tuples, and any other types that you'd otherwise have to write by hand.
You can declare a type alias using the type
keyword:
type Point = {
x: number;
y: number;
};
Here, we've created a type alias Point
for an object type that has x
and y
properties, both of type number
.
Once you've defined a type alias, you can use it in places where you would use any other type:
function drawPoint(p: Point) {
console.log(`The point is at position ${p.x}, ${p.y}`);
}
drawPoint({ x: 10, y: 20 }); // Outputs: "The point is at position 10, 20"
In this function drawPoint
, we've used the Point
type alias as the type for the parameter p
.
In TypeScript, both type
and interface
can be used to define custom types, and they have a lot of overlap.
You can define both simple and complex types with both type
and interface
. For example, you can use both to represent a function type or an object type.
The key differences between type
and interface
are as follows:
TypeScript allows interface declarations with the same name to be merged. It's a key advantage of interfaces; but, you can't do this with type aliases.
interface User {
name: string;
}
interface User {
age: number;
}
let user: User = {
name: 'John Doe',
age: 30,
};
type
alias has a few more features than interface
. It supports union (|
), intersection (&
), typeof
, etc, which allows you to create more complex types.
type User = {
name: string;
} & {
age: number;
};
let user: User = {
name: 'John Doe',
age: 30,
};
If you want to create complex type combinations, you'd use type
. If you're describing object shapes, interface
is usually the better choice.
Discriminated Unions, also known as tagged unions or algebraic data types, are types that can represent a value that could be one of several different types. However, each type in the union is marked with a distinct literal type, hence the term 'discriminated'.
Discriminated Unions typically involve a type
or kind
field that acts as the discriminant. Here's how to implement a Discriminated Union:
type Cat = {
type: 'cat';
breeds: string;
};
type Dog = {
type: 'dog';
breeds: string;
};
type Pet = Cat | Dog;
function printPet(pet: Pet) {
switch (pet.type) {
case 'cat':
console.log(`Cat breeds: ${pet.breeds}`);
break;
case 'dog':
console.log(`Dog breeds: ${pet.breeds}`);
break;
}
}
printPet({ type: 'cat', breeds: 'Maine Coon' }); // Outputs: "Cat breeds: Maine Coon"
In this example, Pet
can be either a Cat
or a Dog
. The printPet
function uses the type
field (the discriminant) to determine the specific type of Pet
and access the corresponding breeds
property.
Discriminated Unions are an effective way of handling different types of objects, and they showcase TypeScript's strengths in static type checking. They also enable you to work with only the data that's necessary for a given section of your code, increasing functionality and efficiency.
Method overloading allows for multiple methods with the same name but different parameters or types. This then provides the ability to manipulate data in different ways based on the input type.
For example
In TypeScript, method overloading is a bit different than in other languages. Here's how it's done:
class Logger {
// Method overload signatures
log(message: string): void;
log(message: object): void;
// Implementation of the method
log(message: string | object): void {
if (typeof message === 'string') {
console.log(`String message: ${message}`);
} else if (typeof message === 'object') {
console.log(`Object message: ${JSON.stringify(message)}`);
}
}
}
const logger = new Logger();
logger.log("This is a string message.");
// Output: String message: This is a string message.
logger.log({ key: "value", anotherKey: 123 });
// Output: Object message: {"key":"value","anotherKey":123}
In the Logger
class, we're declaring two overloaded methods log(message: string)
and log(message: object)
. We then have an implementation of the logger
method that accepts either a string
or object
type.
This implementation will be used when the logger
method is called inside the implementation, and we use type guards to check the type of message
and handle each case separately.
While method overloading in TypeScript allows you to create cleaner and more explicit APIs, it's essential to note that TypeScript does this syntactic checking at compile time and then compiles the overloads into a single JavaScript function.
In TypeScript, a mapped type is a generic type which uses a union created from another type (usually an interface) to compute a set of properties for a new type.
Here's an example of a mapped type that turns all properties in T
into readonly properties. We can use it to create a read-only User
:
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type User = {
name: string;
age: number;
};
let user: ReadOnly<User> = {
name: "John",
age: 30
};
user.name = "Jim"; // Error: Cannot assign to 'name' because it is a read-only property
Mapped types allow you to create new types based on old ones by transforming properties. They're a great way to keep your code DRY (Don't Repeat Yourself), making your code more maintainable and easier to read.
Conditional types in TypeScript allow you to introduce type variables and type inference within types, making them incredibly powerful for building complex type logic.
A conditional type selects one of two possible types based on a condition expressed as a type relationship test. They are written in the form T extends U ? X : Y
. So if T extends U
is true, then the type X
is selected. But if T extends U
is false, then Y
is selected.
Here's an example:
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
'object';
type T0 = TypeName<string>; // "string"
type T1 = TypeName<'a'>; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "object"
Here, TypeName<T>
is a conditional type that checks if T
extends certain types and resolves to a string literal of the type name.
Conditional types can be chained with more conditional types to create more complex type relations. Also, TypeScript includes an infer
keyword, that allows you to infer a type inside your conditional type.
TypeScript supports rest parameters and spread operators, similar to ES6.
A rest parameter is a function parameter that allows you to pass an arbitrary number of arguments to the function. In TypeScript, you can include type annotations for rest parameters.
For example:
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4, 5); // Outputs: 15
In this example:
...numbers: number[]
is a rest parameternumbers
The spread operator is used to spread elements of an array or object properties of an object. In TypeScript, you can spread arrays of any type.
For example:
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5, 6]; // [1, 2, 3, 4, 5, 6]
In this example:
...arr1
is a spread operatorarr1
and spreads its elements into a new array arr2
By utilizing rest parameters and spread operators, TypeScript enables developers to write more expressive and flexible code, especially when dealing with arrays and function arguments.
Rest parameters allow functions to handle an indefinite number of arguments, while spread operators enable more expressive and concise code when working with arrays and objects
These features enhance code reusability and readability, making it easier to work with collections of data
keyof
keyword and how is it used in TypeScript?In TypeScript, the keyof
keyword is an index type query operator. It yields a union type of all the known, public property names of its operand type.
Let's take a look at an example:
interface User {
name: string;
age: number;
}
type UserProps = keyof User; // "name" | "age"
In this example, UserProps
will be a type representing all property names of User
, i.e., the union type "name" | "age"
.
The keyof
keyword is handy here when you want to write functions that can handle any property of an object, regardless of its name.
For example:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let user: User = {
name: 'John Doe',
age: 30,
};
let userName: string = getProperty(user, 'name');
let userAge: number = getProperty(user, 'age');
In this getProperty
function, the keyof
keyword is used to ensure that the key
argument is a valid property name of obj
. The function then returns the value of that property.
TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally. Some of the most commonly used utility types are Partial
, Readonly
, and Record
.
The Partial
type takes in another type as its parameter, and makes all of its properties optional.
interface User {
name: string;
age: number;
}
let partialUser: Partial<User> = { name: 'John Doe' };
Here, partialUser
is of type Partial<User>
, so both name
and age
properties are optional.
The Readonly
type makes all properties of a type readonly, meaning they can't be reassigned after creation.
let readonlyUser: Readonly<User> = { name: 'John Doe', age: 30 };
readonlyUser.age = 31; // Error: Index signature in type 'Readonly<User>' only permits reading
The Record
utility type helps create a map where all entries are keyed on a specified type. This is useful when you want to associate some key (like a database ID) with a value (record):
interface User {
│ name: string;
│ age: number;
}
type UserRecord = Record<number, User>;
let users: UserRecord = {
1: {
name: 'John Doe',
age: 30
},
2: {
name: 'Jane Doe',
age: 29
}
};
In this example, UserRecord
is an object type containing values of type User
keyed on a number.
Exception handling in TypeScript is similar to JavaScript. You can use try
/catch
/finally
blocks to handle errors and exceptions.
Here's a simple example:
try {
throw new Error('This is an error!');
}
catch (error) {
console.error(error.message);
}
finally {
console.log('This always runs!');
}
In this example:
try
block throws an errorcatch
block catches the error and logs its message to the consolefinally
block always runs, regardless of whether an error occurred or notTypeScript also brings static types to JavaScript, and you can use them in error handling. For instance, you can create custom error classes that extend the built-in Error
class and add custom properties to them.
For example:
class CustomError extends Error {
constructor(public code: number, message?: string) {
super(message);
}
}
try {
throw new CustomError(404, 'Resource not found');
}
catch (error) {
if (error instanceof CustomError) {
console.error(`Error ${error.code}: ${error.message}`);
}
}
In this example:
CustomError
class extends Error
and adds a code
property to itcatch
block checks if the error is an instance of CustomError
CustomError
, it logs the error code and message to the consoleUsing TypeScript's static typing and custom error classes enhances type safety, provides clearer error messages, and improves debugging by offering more context-specific information about the error.
This approach allows you to create more robust and maintainable error-handling mechanisms in your code.
In TypeScript, a generic class refers to a class that can work with a variety of data types, rather than a single one. This allows users of the class to specify the type they want to use.
Here's an example of a generic class:
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
remove(item: T): void {
const index = this.data.indexOf(item);
if (index > -1) {
this.data.splice(index, 1);
}
}
getAll(): T[] {
return this.data;
}
}
In this example:
DataStore
is a generic class, denoted by the <T>
next to its name.T
represents any type.T
is used as if it were a regular type.When we create an instance of DataStore
, we can specify what T
should be.
For example:
Here’s how we can create a DataStore
that works with numbers:\
const numberStore = new DataStore<number>();
numberStore.add(5);
console.log(numberStore.getAll()); // Outputs: [ 5 ]
numberStore.remove(5);
console.log(numberStore.getAll()); // Outputs: []
Dynamic module loading in TypeScript is supported through the ES6 dynamic import expression. The dynamic import()
expression allows us to load modules on-demand, which can result in significant performance benefits by minimizing the startup cost of your application.
The dynamic import expression returns a promise that resolves to the module. Here is a basic example:
// Loads the module "./utils" dynamically
import('./utils').then((utils) => {
// Use the module
console.log(utils.divide(10, 2)); // Outputs: 5
}).catch((error) => {
console.log(`An error occurred while loading the module: ${error}`);
});
In this example, the utils
module is loaded dynamically. Once it's loaded, the promise is resolved, and the then
callback is executed, where we can use the module. If an error occurs while loading the module, the catch
callback is triggered.
Dynamic module loading provides a way to load modules on the fly, which can be useful for code splitting or lazy loading modules in an application.
In TypeScript, index types allow you to create complex types that are based on the properties of another type.
The keyof
operator is used to create a type representing the property names of a given type:
type Person = {
name: string;
age: number;
};
type PersonKeys = keyof Person; // "name" | "age"
The indexed access operator
is used to access the property type of a specific type:
type PersonNameType = Person['name']; // string
Putting these together, you could create a function that takes an object, a property of that object, and a new value for the property, and types it correctly:
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
let person: Person = {
name: 'John',
age: 30
};
setProperty(person, 'name', 'Jane'); // OK
setProperty(person, 'age', '35'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
This function ensures type safety, meaning that it only allows setting properties that exist on the object, and the value must be of the right type.
TypeScript provides built-in utility types ReadonlyArray<T>
and Readonly<T>
for representing read-only array and object types, respectively.
You can define a read-only array using the ReadonlyArray<T>
generic type where T
is the type of array elements:
const arr: ReadonlyArray<number> = [1, 2, 3, 4, 5];
However, once defined as ReadonlyArray
, you can't alter the array. This means that any attempt to mutate the array such as push, pop, or assignment will result in a compile-time error:
arr.push(6); // Error: Property 'push' does not exist on type 'readonly number[]'.
arr[0] = 10; // Error: Index signature in type 'readonly number[]' only permits reading.
Similarly, you can make the properties of an object read-only using the Readonly<T>
generic utility type where T
is the type of the object:
type Point = {
x: number;
y: number;
};
const point: Readonly<Point> = {
x: 10,
y: 20
};
Any attempt to change the properties of point
will result in a compile-time error:
point.x = 30; // Error: Cannot assign to 'x' because it is a read-only property.
The Readonly
and ReadonlyArray
types ensure immutability, which is particularly useful in functional programming and can be beneficial for state management in large applications like those made with React and Redux.
The never
type in TypeScript represents a value that never occurs. It is the return type for functions that never return or for functions that always throw an exception.
For instance, if you have a function that always throws an error and never returns a value, you can type it like this:
function throwError(message: string): never {
throw new Error(message);
}
Similarly, for a function that contains an infinite loop and never reaches its end:
function infiniteLoop(): never {
while(true) {
console.log('Never ending loop');
}
}
The never
type is a subtype of all types and can be assigned to any other types; however, no types can be assigned to never
(except never
itself).
This can be useful for exhaustiveness checks in switch-case constructs where TypeScript can ensure that every case is handled.
When working with external JavaScript libraries that do not have type definitions, TypeScript might display errors since it cannot understand the types from these libraries.
To get around this, you can create a declaration file with a .d.ts
extension in your project to tell TypeScript how the module is shaped. For instance, if you are using a JavaScript library called jsLibrary
, you would create a jsLibrary.d.ts
file:
declare module 'jsLibrary';
By declaring the module, you tell TypeScript to treat this module as any
type and thus bypass the type checking.
For more complex libraries, TypeScript provides an option to install type definitions from DefinitelyTyped using npm also:
npm install --save @types/library-name
Here library-name
is the name of the JavaScript library, and the @types
organization on npm is managed by the DefinitelyTyped project, which provides high-quality TypeScript definitions for a multitude of JavaScript libraries.
However, not all libraries have types available on DefinitelyTyped, and in such cases, creating your own declaration file is the way to go.
To create a more detailed declaration file, you can define the library's types and functions:
declare module 'jsLibrary' {
export function libFunction(arg: string): number;
}
This declares a function libFunction
in 'jsLibrary'
that takes a string
and returns a number
. You can add more functions and types as required by the library you're using.
Ambient declarations are a way of telling the TypeScript compiler that some other code not written in TypeScript will be running in your program.
With the help of ambient declarations, you can opt out of the type checking for certain values, or inform the compiler about types it can't know about. This is commonly done in a .d.ts
file, or an ambient context file, where you can declare types, variables, classes or even modules.
Here's an example of an ambient variable declaration, that might be found in a .d.ts
file:
declare let process: {
env: {
NODE_ENV: 'development' | 'production';
[key: string]: string | undefined;
}
};
In this example, we're declaring a global process
object that has a property env
, which is another object. This env
object has a known property NODE_ENV
that can either be 'development' or 'production', and can have any number of string properties.
You can also use ambient declarations to describe types of values that come from a third-party JavaScript library, which doesn't provide its own type definitions.
Here's an example of how to declare a class in a third-party library:
declare class SomeLibrary {
constructor(options: { verbose: boolean });
doSomething(input: string): number;
}
Here we're telling TypeScript that there will be a SomeLibrary
class, it has a constructor that takes an options object, and has a doSomething
method.
This helps ensure type safety when using the SomeLibrary
class in your TypeScript code.
TypeScript declaration files (.d.ts
files) are a means to provide a type layer on top of existing JavaScript libraries, or for defining types for environmental variables that exist in runtime environments outside of our own TypeScript code.
In simple terms, a declaration file is for telling TypeScript that we are using some external module that isn't written in TypeScript.
Here is an example of a declaration file (index.d.ts
) for a module named someModule
:
declare module 'someModule' {
export function someFunction(input: string): string;
}
This index.d.ts
file tells TypeScript about the shape and the types used in the someModule
module.
When you use the someModule
in your TypeScript code like this:
import { someFunction } from 'someModule';
let result: string = someFunction('test');
TypeScript will use your declaration file to check that your usage of someModule
is correct - that you're importing a function that does exist in the module, and that you're using the result as a string
.
A TypeScript declaration file can be complex and can contain declarations for modules, classes, functions, variables, etc. They serve a crucial role when using JavaScript libraries in TypeScript, or when you need to use global variables that exist in the runtime environment.
In TypeScript, function overloading or method overloading is the ability to create multiple methods with the same name but with different parameter types and return type.
For example:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
if (typeof a === 'string' && typeof b === 'string') {
return a.concat(b);
}
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
}
let result1 = add(10, 20); // returns 30
let result2 = add('Hello', ' World'); // returns 'Hello World'
Here, the add
function is overloaded with different parameter types.
When two numbers are passed, it returns the sum of numbers When two strings are passed, it concatenates and returns the string
The compiler uses the number of parameters and their types to determine the correct function to call.
Function overloading allows you to call the same function in different ways, which can make your code easier to read and use. However, you should take care not to overload functions too often, as it can make your code more complex and harder to maintain.
Always make sure to document your function's behavior well, so other developers know what to expect when they use your overloaded function.
Type intersection in TypeScript is a way to combine multiple types into one. The resulting type has all the properties and methods of the intersected types. This is particularly useful when you want to create a new type that combines the features of multiple existing types.
The syntax for creating an intersection type is to use the &
operator between the types you want to combine:
interface User {
name: string;
age: number;
}
interface Admin {
adminLevel: number;
}
type AdminUser = User & Admin;
const admin: AdminUser = {
name: "Alice",
age: 30,
adminLevel: 1
};
In this example, the AdminUser
type combines the properties of both User
and Admin
.
Did you get all 41 correct?
It’s ok if you couldn’t answer all of these. Not even the most senior programmers have all the answers, and need to Google things!
You're interviewer won't expect you to know everything.
Whats most important is to answer interview questions with confidence, and if you don't know the answer, use your experience to talk through how you're thinking about the problem and ask follow-up questions if you need some help.
This shows the interviewer that you can think through situations instead of giving up and saying "I don't know". They don't expect you to know 100%, but they do want people who can adapt and figure things out.
Someone who puts in the effort to answer questions outside their scope of knowledge is much more likely to get hired than someone who gives up at the first sign of trouble.
So good luck, and remember to stay calm. You’ve got this!
If absolutely none of these questions made sense, or if you simply want to dive deeper into TypeScript and build some more impressive projects for your portfolio, then come and check out my complete TypeScript Developer course.
Taught by an industry professional (me!), this course covers everything from beginner to advanced topics. So if you're a JavaScript developer who is serious about taking your coding skills and career to the next level, then this is the course for you.
You’ll not only get access to step-by-step tutorials, but you can ask questions from me and other instructors, as well as other students inside our private Discord.
Either way, if you decide to join or not, good luck with your interview and go get that job!
If you've made it this far, you're clearly interested in TypeScript so definitely check out all of my TypeScript posts and content: