TypeScript
TypeScript is a strongly typed programming language that builds on JavaScript. It adds optional static typing, classes, and modules to JavaScript, making it easier to develop large-scale applications. TypeScript compiles to clean, simple JavaScript code which runs on any browser, in Node.js, or in any JavaScript engine that supports ECMAScript 3 or newer.
2. Disadvantages of TypeScript
While TypeScript offers many benefits, it also has some drawbacks:
- Additional Compilation Step: TypeScript needs to be compiled to JavaScript, which adds an extra step in the development process.
- Learning Curve: Developers familiar with JavaScript need to learn new concepts like static typing.
- Overly Complex Type System: It's possible to create very complex type definitions that can be hard to maintain.
- Tooling Dependence: To get the most out of TypeScript, you need good tooling support, which may not be available in all environments.
3. Statically Typed Language
TypeScript is a statically typed language, which means the type of a variable is known at compile time. This is in contrast to JavaScript, which is dynamically typed. Static typing can help catch errors early in the development process and provide better tooling support.
4. Compiling a TypeScript Project
To compile a TypeScript file, you use the TypeScript compiler (tsc):
This will generate a JavaScript file (index.js
) from your TypeScript file.
5. Setting Types
In TypeScript, you can explicitly set types:
6. Types in TypeScript
Implicit vs Explicit Types
- Implicit: TypeScript can infer types based on the assigned value.
- Explicit: You can manually specify the type.
Any Type
The any
type is a way to opt-out of type checking:
Unknown Type
The unknown
type is similar to any
, but safer because it's not legal to do anything with an unknown
value:
Never Type
The never
type represents the type of values that never occur:
Enum
Enums allow us to define a set of named constants:
Tuple
Tuples allow you to express an array with a fixed number of elements whose types are known:
7. Objects
Object Types
You can define the shape of an object using object types:
Methods
Objects can have methods:
Specific Values
You can specify that an object property must have a specific value:
Return Type
You can specify the return type of a function:
8. Type Aliases
Type aliases create a new name for a type:
9. Union Types
Union types allow a value to be one of several types:
10. Type Intersection
Intersection types combine multiple types into one:
11. Literal Types
Literal types allow you to specify exact values:
12. Nullable Types
You can explicitly allow null
or undefined
:
13. Optional Properties, Elements, and Calls
You can mark properties, elements, or function parameters as optional using ?
:
14. Interfaces
Interfaces define the structure that objects must adhere to:
Reopening Interfaces
You can add new properties to an existing interface:
interface Person {
email: string;
}
let user: Person = { name: "John", age: 30, email: "[email protected]" };
|
Interface Inheritance
Interfaces can extend other interfaces:
15. Classes
Classes in TypeScript are similar to those in other object-oriented languages:
Modifiers
TypeScript supports access modifiers:
public
(default)private
protected
Getters and Setters
You can use getters and setters to intercept access to a member of an object:
Abstract Classes
Abstract classes are base classes from which other classes may be derived:
Method Overriding
Derived classes can override methods from their base class:
Difference Between Class and Abstract Class
- Regular classes can be instantiated, while abstract classes cannot.
- Abstract classes may contain abstract methods (without implementation) that must be implemented in derived classes.
16. Generics
Generics allow you to write reusable, type-safe code:
Generics can be used with functions, classes, and interfaces to create reusable components.
Generics in TypeScript
Generics provide a way to create reusable components that can work with a variety of types rather than a single one. They allow you to write functions, classes, and interfaces that can work with any data type while still providing compile-time type checking.
1. Generic Functions
Generic functions allow you to use type variables that are determined when the function is called.
Basic Example:
Multiple Type Parameters:
2. Generic Interfaces
You can create generic interfaces to describe object shapes with flexible types.
3. Generic Classes
Generic classes allow you to have type parameters for the entire class.
4. Generic Constraints
You can constrain the types that can be used with a generic using the extends
keyword.
5. Using Type Parameters in Generic Constraints
You can declare a type parameter that is constrained by another type parameter.
6. Generic Parameter Defaults
TypeScript allows you to specify default types for type parameters.
7. Generic Utility Types
TypeScript provides several utility types that use generics:
Partial<T>
Makes all properties in T optional:
Record<K,T>
Constructs a type with a set of properties K of type T:
Pick<T,K>
Constructs a type by picking the set of properties K from T:
8. Conditional Types with Generics
Conditional types allow you to create more complex type relationships:
Type vs Interface in TypeScript
While type
and interface
in TypeScript can often be used interchangeably, there are some key differences between them. Let's explore these differences:
1. Basic Syntax
Type:
Interface:
2. Extending/Inheritance
Type:
Types use intersection to extend:
Interface:
Interfaces use the extends
keyword:
3. Declaration Merging
Type:
Types cannot be re-opened to add new properties.
Interface:
Interfaces can be extended after being defined (declaration merging):
4. Computed Properties
Type:
Can have computed properties:
Interface:
Cannot have computed property names:
5. Union Types
Type:
Can define union types:
Interface:
Cannot define union types (but can use them):
6. Utility Types
Type:
Works well with utility types:
Interface:
Can be used with utility types, but less commonly:
7. Tuples
Type:
Can easily describe tuples:
Interface:
Can describe tuples, but it's more verbose:
8. Implements
Both can be implemented by classes:
9. Performance
For very complex types, the TypeScript compiler might be slightly faster with interfaces, but this is rarely a concern in practice.
Conclusion
In general:
- Use
interface
when you want to define a contract for an object shape, especially if you might want to extend it later. - Use
type
when you need to create complex types involving unions, intersections, or mapped types.
Both type
and interface
can be used in many situations, and the choice often comes down to personal or team preference. The TypeScript team generally recommends using interface
until you need to use features from type
.
Interface vs Class
Direct Answer
In TypeScript, both classes and interfaces serve as blueprints for objects, but they differ significantly in their capabilities and intended use cases.
- Classes are a fundamental part of object-oriented programming in TypeScript. They can contain properties, methods, and constructors. Classes support inheritance, allowing one class to inherit properties and methods from another. They also allow for member visibility control (public, private, protected). Importantly, classes exist in the compiled JavaScript output, meaning they contribute to the runtime behavior of your application.
- Interfaces, on the other hand, are purely a TypeScript construct used for type-checking during compile time. They define the shape of an object—what properties and methods an object should have—but do not provide any implementation. Since interfaces are removed during the compilation process, they do not appear in the resulting JavaScript code. This makes them ideal for defining data shapes without adding any runtime overhead.
Example Code
Using an Interface
Using a Class
Access Modifiers
Access modifiers in TypeScript are keywords that set the accessibility level of class members (properties and methods). There are three types of access modifiers:
public
private
protected
By default, all members of a class in TypeScript are public
if no modifier is specified. Let's explore each modifier in detail with examples.
Public
Members declared as public
can be accessed anywhere without restrictions. This is the default access level if none is specified.
Private
Members marked as private
can only be accessed within the class they are declared. Attempting to access them outside the class will result in a compile-time error.
Protected
Members marked as protected
can be accessed within the class they are declared and by subclasses. Like private
, attempting to access protected
members outside these bounds will result in a compile-time error.
Summary
- Public: Members are accessible everywhere. This is the default.
- Private: Members are only accessible within the class they are declared.
- Protected: Members are accessible within the class they are declared and by subclasses.
Data Types
TypeScript provides a rich set of data types that enable developers to specify the type of data that variables, parameters, and object properties can store.
These types range from primitive types like number, string, and boolean to more complex types such as enum
, array
, tuple
, and user-defined types like classes
and interfaces
.
Primitive Types
Number
TypeScript supports numeric values as numbers. This includes both integers and floating-point values.
String
Strings represent textual data. They can be enclosed in single quotes (' '), double quotes (" "), or backticks () for template literals.
Boolean
Booleans represent true/false values.
Special Types
Any
The any
type is a powerful way to work with existing JavaScript, allowing you to opt-out of type-checking.
Unknown
Similar to any
, but safer because anything is assignable to unknown
, but unknown
isn't assignable to anything but itself and any
without a type assertion or a control flow based narrowing.
Complex Types
Enum
Enums allow us to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases.
Array
Arrays can be declared in two ways: using the type of the elements followed by []
, or using a generic array type Array<elemType>
.
Tuple
Tuples allow you to express an array with a fixed number of elements whose types are known, but need not be the same.
User-defined Types
Interface
Interfaces define the shape of an object. They are purely a compile-time construct and get erased when transpiling to JavaScript.
Class
Classes support inheritance, short syntax for creating objects and dealing with inheritance, and will be familiar if you're coming from an object-oriented language.
4 Pillars of Object Oriented Programming in TypeScript
The four pillars of Object-Oriented Programming (OOP) are Encapsulation, Inheritance, Polymorphism, and Abstraction.
Encapsulation
Encapsulation is the bundling of data, represented by properties, and methods that operate on that data into a single unit called a class. It also involves restricting access to some of the object's components, which is known as information hiding.
Inheritance
Inheritance is a mechanism where one class acquires the properties and methods of another class. The class being inherited from is called the superclass or parent class, and the class doing the inheriting is called the subclass or child class.
Polymorphism
Polymorphism allows objects of different classes to be treated as objects of a common superclass. It enables one interface to represent a general class of actions, which means that the exact action is determined by the exact nature of the situation.
Abstraction
Abstraction means hiding complex details and showing only the essentials. In TypeScript, abstraction can be achieved using abstract classes and interfaces. An abstract class cannot be instantiated directly but can be extended by other classes.
Dependency Injection (DI) in TypeScript
Dependency Injection (DI) is a design pattern that promotes loose coupling between classes and their dependencies. Instead of hard-coding dependencies inside a class, DI allows dependencies to be injected into a class, usually through constructors or methods. This pattern enhances modularity, testability, and maintainability of the code. TypeScript, with its strong typing features, complements DI well, making it easier to manage dependencies explicitly.
Basic Dependency Injection Example
Consider a scenario where ServiceB
depends on ServiceA
. Without dependency injection, ServiceB
might instantiate ServiceA
directly within its own code, leading to tight coupling. With DI, ServiceA
is passed to ServiceB
, often through the constructor.
In this example, ServiceB
receives an instance of ServiceA
through its constructor, allowing it to call doSomething()
without knowing how to create or manage ServiceA
.
Using Dependency Injection Containers
While manual dependency injection (as shown above) is straightforward, managing dependencies can become complex in larger applications. Dependency Injection Containers (DI Containers), also known as Inversion of Control (IoC) containers, automate the process of creating objects and injecting their dependencies.
DI Containers:
- Manage object creation and lifecycle.
- Automatically resolve and inject dependencies.
- Can be configured programmatically or via configuration files.
Several libraries offer DI Container functionality for TypeScript, such as InversifyJS, NestJS (which uses InversifyJS under the hood), and Microsoft's TypeScript DI container. These containers allow for more sophisticated dependency management, including handling circular dependencies, lazy initialization, and scoped lifetimes.
Decorators
Decorators in TypeScript are a powerful feature that allows you to add annotations and a meta-programming syntax for class declarations and members. Introduced as an experimental feature in TypeScript, decorators have evolved and are now supported natively in TypeScript 5.0, aligning with the ECMAScript Stage 3 proposal. This feature enables developers to modify or augment classes and class members at design time, offering a wide range of use cases from logging to dependency injection.
Enabling Decorators in TypeScript
Before TypeScript 5.0, decorators were considered an experimental feature and needed to be explicitly enabled in the tsconfig.json
file by setting "experimentalDecorators": true
. However, starting from TypeScript 5.0, decorators are enabled by default, and the experimental flag is no longer required.
Types of Decorators
There are four primary types of decorators in TypeScript:
- Class Decorators: Applied to the class declaration itself.
- Method Decorators: Applied to the constructor, method, accessor, property, or parameter of a class.
- Property Decorators: Applied to the property of a class.
- Accessor Decorators: Applied to the accessor of a class.
Examples
Class Decorator
A class decorator is applied to the class definition. It can be used to observe, modify, or replace a class definition.
Method Decorator
A method decorator is applied to the constructor, method, accessor, property, or parameter of a class.
Property Decorator
A property decorator is applied to the property of a class.
Accessor Decorator
An accessor decorator is applied to the accessor of a class.
Type Assertion
Type assertion in TypeScript is a way to tell the compiler "trust me, I know what I'm doing." It allows you to override the inferred type of a variable, essentially letting you inform the compiler about the type of a variable. This can be particularly useful when you have a better understanding of the type of a variable than what TypeScript can infer on its own, especially when migrating code from JavaScript to TypeScript.
Syntax
Type assertion can be performed in two ways:
- Using Angle Brackets:
<Type>variableName
- Using the
as
Keyword:variableName as Type
Example: Type Assertion with Numbers
Or using the as
keyword:
In both examples, we assert that code
is of type number
, even though it was initially declared as any
. This tells the TypeScript compiler to treat employeeCode
as a number
.
Example: Type Assertion with Objects
Suppose you have an object that hasn't been defined with any properties yet, but you know it will have certain properties later on.
Here, we assert that foo
is of type Foo
, allowing us to add properties bar
and bas
to it without TypeScript errors.
Double Assertion
Sometimes, you might encounter situations where TypeScript requires a double assertion. This is less common but can be necessary in certain scenarios, such as when you need to assert a variable to a more specific type that is not directly assignable from the current type.
In this example, event
is of type Event
, but we want to treat it as an HTMLElement
. First, we assert it to unknown
(which is compatible with all types), and then to HTMLElement
.
Type Annotation
Type annotations in TypeScript are a way to explicitly specify the type of variables, function parameters, and object properties. TypeScript, being a statically typed superset of JavaScript, introduces type annotations to help developers catch errors early during development, improve code readability, and ensure that the code behaves as expected.
Declaring Variables with Type Annotations
You can declare variables with type annotations by specifying the type after the variable name, preceded by a colon. This informs the TypeScript compiler about the expected type of the variable, enforcing type safety.
Attempting to assign a value of a different type to these variables would result in a TypeScript compilation error, helping to prevent bugs related to incorrect data types.
Type Annotations in Functions
Function parameters and return types can also be annotated with types. This ensures that functions receive the correct types of arguments and specifies what type of value the function returns.
In this example, the display
function expects two parameters: id
of type number
and name
of type string
. The function does not return a value, hence the return type is void
.
Inline Type Annotations for Object Properties
Type annotations can be used to specify the types of properties within an object. This is particularly useful when defining the shape of an object, ensuring that all properties adhere to the expected types.
In this case, the employee
object is expected to have an id
property of type number
and a name
property of type string
. Assigning values of different types to these properties would result in a TypeScript error.
Why Use Type Annotations?
- Type Safety: Type annotations help catch errors early by preventing assignments of incompatible types.
- Readability and Maintenance: Explicitly stating the types of variables, parameters, and properties makes the code easier to read and maintain.
- Tooling Support: Modern IDEs and editors can leverage type annotations to provide better autocompletion, refactoring tools, and inline documentation.
Type Guarding, Type Unknown, Type Any & Type Never
TypeScript introduces several unique types that help developers handle various programming scenarios more effectively. Among these, unknown
, any
, and never
stand out for their utility in different contexts. Additionally, type guarding plays a crucial role in narrowing down types, especially when dealing with the unknown
type. Let's delve into each of these concepts in detail.
Type Guarding
Type guards are expressions that perform runtime checks to guarantee the type within a certain scope. They are essential for working with the unknown
type, as they allow you to safely access properties or methods that are specific to a particular type.
In this example, isString
is a type guard that narrows the type of test
to string
within the block where it's used. This is particularly useful when dealing with values of type unknown
, as it allows for safe type checking and operation.
Type Unknown
The unknown
type represents any value but is safer than any
because you cannot perform arbitrary operations on values of type unknown
without first asserting or narrowing to a more specific type.
In prettyPrint
, x
is of type unknown
, but by using type guards (typeof x === "string"
and Array.isArray(x)
), TypeScript can safely determine the type within each branch, allowing for accurate type-checking and operations.
Type Any
The any
type is a powerful way to bypass TypeScript's static type checking. It essentially opts out of type checking for a variable, allowing it to hold any type of value. However, using any
defeats the purpose of using TypeScript, as it removes the benefits of type safety.
While any
can be useful in certain scenarios, it's generally recommended to avoid it unless absolutely necessary, as it undermines the type safety that TypeScript aims to provide.
Type Never
The never
type represents values that never occur. For example, it's the return type of functions that never return a value (e.g., functions that throw an exception or enter an infinite loop).
In this example, error
is a function that takes a message
of type string
and throws an error. Since throwing an error prevents normal execution flow, the return type of error
is never
.
Fetch API in TS and Declarations File
Fetching data in TypeScript, especially when dealing with external APIs, can be efficiently managed using the fetch
API or libraries like node-fetch
for Node.js environments. To ensure smooth integration and type safety, it's crucial to understand how to correctly declare and use these functionalities in TypeScript.
Using Fetch in TypeScript
The fetch
API is widely used for making network requests in web browsers. When migrating to TypeScript, you might encounter issues with type definitions, especially since the native fetch
API doesn't come with TypeScript typings by default.
To use fetch
in TypeScript, you typically don't need to do anything special if you're running your code in a browser environment, as browsers already include the fetch
API. However, for Node.js environments, you'd use node-fetch
or similar libraries, which provide their own type definitions.
Using node-fetch in TypeScript
node-fetch
is a light-weight module that brings the fetch
API to Node.js, making it easy to make HTTP requests. To use node-fetch
in TypeScript, you first need to install it and its type definitions.
- Install
node-fetch
and its type definitions:
-
Import
fetch
in your TypeScript file:
Ensure your
tsconfig.json
is configured to accept modules:
- Use
fetch
to make HTTP requests:
Handling Type Definitions
When using node-fetch
or similar libraries, TypeScript might complain about missing type definitions. This can be resolved by installing the corresponding type definitions package, such as @types/node-fetch
for node-fetch
.
However, as noted in Source 4, newer versions of node-fetch
(v3 and above) bundle their own type definitions, so you might not need to install @types/node-fetch
separately. Always check the library's documentation for the latest guidance on type definitions.
Declaration Files
Declaration files (.d.ts
) in TypeScript provide type information about an API that isn't available in the TypeScript standard library. When you encounter a TypeScript error saying it couldn't find a declaration file for a module, it means TypeScript is expecting type definitions for that module but can't find them.
To fix this, you can manually download the type definitions from DefinitelyTyped (if available) or install them via npm (for libraries that include their own type definitions or have community-provided ones).
Type Alias & Intersection Type
Type Alias
Type alias in TypeScript allows you to create a new name for a type. This can be particularly useful for simplifying complex type definitions, reusing types across your project, or making your code more readable. You define a type alias using the type
keyword followed by the alias name and the type it represents.
Example of Type Alias
In this example, StringOrNumber
is a type alias for the union type string | number
. This makes the processInput
function signature more readable and reusable.
Intersection Type
Intersection types in TypeScript allow you to combine multiple types into one. This is useful when you want an object to satisfy multiple interfaces or when you want to combine primitive types. The combined type will have all the properties of the constituent types.
Defining Intersection Types
You define an intersection type using the &
operator between the types you want to combine.
In this example, EmployeePerson
is an intersection type that combines Person
and Employee
. An object of type EmployeePerson
must have both name
and employeeID
properties.
Important Notes
- The order of types in an intersection does not affect the resulting type.
- If there are overlapping properties in the types being combined, the resulting type will have those properties, potentially causing conflicts if not handled carefully.
Advanced Usage
Intersection types can be nested or combined with other advanced types, making them a powerful tool for modeling complex relationships between types.
In this advanced example, EmployeeWithAddress
is an intersection type that combines Employee
with an optional address
property. This demonstrates how intersection types can be used to extend existing types with additional properties.
tsconfig.json
The tsconfig.json
file configures TypeScript compiler options for a project, specifying how TypeScript should behave when compiling the source code.
sample
tsconfig.json
Compiler Options
target
: Specifies the ECMAScript target version. Here, it's set toes5
, meaning the output will be compatible with ES5 environments.lib
: Defines library files to be included in the compilation. Here, it includes DOM APIs (dom
), iterable protocol support for DOM collections (dom.iterable
), and the latest ECMAScript features (esnext
).allowJs
: Allows JavaScript files to be compiled along with TypeScript files. This is useful when integrating existing JavaScript code with TypeScript.skipLibCheck
: Skips type checking of declaration files (*.d.ts). This can speed up compilation times.esModuleInterop
: Enables a more compatible CommonJS/AMD module import/export syntax. This is particularly useful when importing CommonJS modules in TypeScript projects.allowSyntheticDefaultImports
: Allows default imports from modules with no default export. This can make importing CommonJS modules more convenient.strict
: Enables all strict type-checking options. This makes TypeScript enforce stricter type checking, helping catch potential bugs at compile time.forceConsistentCasingInFileNames
: Ensures that references to the same file are consistent in casing. This helps avoid errors caused by inconsistent casing in imports.noFallthroughCasesInSwitch
: Prevents fall-through cases in switch statements without an explicit break statement. This improves code safety and readability.module
: Specifies the module code generation. Here, it's set toesnext
, meaning the latest ECMAScript module syntax will be used.moduleResolution
: Determines how the compiler resolves module imports.node
mode mimics Node.js module resolution strategy.resolveJsonModule
: Allows importing JSON modules. This enables importing JSON files directly in TypeScript.isolatedModules
: Requires each file to be self-contained and not rely on external declarations. This is useful for tree-shaking and optimizing output.noEmit
: Prevents the compiler from emitting output files. This can be used for type checking without generating JavaScript files.jsx
: Specifies how JSX syntax should be compiled. Here, it's set toreact-jsx
, indicating the use of React 17 or later syntax.
Include and Exclude
include
: Specifies which files should be included in the compilation. Here, it includes TypeScript files (*.ts
), React TypeScript files (*.tsx
), and any TypeScript declaration file in thenext-env.d.ts
directory within thesrc
folder.exclude
: Specifies directories to exclude from the compilation process. Here, it excludes thenode_modules
directory.