
How to use Typescript
In the last post we talked about the benefits of typesript and how its possible to use typescript without any special syntax or compilation steps. But using typescript without the compiler can be limiting. In this post we will go over how to fully advantage of the benefits of typescript.
Primitive Types
In TypeScript, primitive types are the most basic data types that represent simple values. They are immutable, meaning their values cannot be changed after they are created. The main primitive types in TypeScript include:
A number
represents both integer and floating-point numbers. Here, age
and price
are both of the number type.
let age: number = 25;
let price: number = 19.99;
A string
represents a sequence of characters, used for textual data.
let name: string = "Alice";
let greeting: string = `Hello, ${name}!`;
A boolean
represents a logical value that can be either true or false.
let isRaining: boolean = true;
let isSunny: boolean = false;
A null
represents the intentional absence of any object value. selectedItem
is declared as a string that can also be null, indicating that no item has been selected.
let selectedItem: string | null = null;
An undefined
represents an uninitialized variable or missing property. optionalName
is defined as a string that could also be undefined, meaning it hasn’t been assigned a value yet. null and undefined are different from each other, but they are both values that can be used to represent the absence of a value.
let optionalName: string | undefined = undefined;
These primitive types form the building blocks for more complex data structures in TypeScript. Understanding them is essential for working effectively with the language.
Function Types
In TypeScript, typing functions is a powerful feature that enhances code readability and maintainability by enforcing the types of input parameters and the return type. This ensures that functions are used as intended and helps prevent common programming errors.
Basic Function with Typed Parameters and Return Type
function add(a: number, b: number): number {
return a + b;
}
Here, the add
function expects two parameters, a
and b
, both of type number, and it also returns a
number.
Function with No Return Value
function log(message: string): void {
console.log(message);
}
The log function accepts a string parameter and does not return a value, hence the return type is void.
Optional Parameters
function greet(name: string, date?: Date): string {
if (date) {
return `Hello, ${name}! Today is ${date.toDateString()}.`;
} else {
return `Hello, ${name}!`;
}
}
The greet function takes a mandatory string
parameter name
and an optional Date
parameter date
. It returns a string
.
Function with Default Parameters
function buildUrl(base: string, path: string = '/'): string {
return `${base}${path}`;
}
The buildUrl
function takes a string
parameter base
and an optional parameter path
with a default value of '/'
. It returns a string.
Function Type for Variable
Functions can be assigned to variables, and you can define the function type explicitly.
let compute: (x: number, y: number) => number;
compute = function(x, y) { return x + y; };
Object Types
Type aliases in TypeScript are a powerful feature that allows you to create a new name for a type. Type aliases are especially useful when you want to simplify complex type definitions or when you need to reuse the same type across your codebase without repeating it.
Type aliases are created using the type
keyword followed by the alias name and the type definition. Once defined, the alias can be used anywhere a type can be used.
Simple Type Alias for an Object
You can define a type alias for an object type to make object structures easier to manage and reuse.
type User = {
id: number;
username: string;
isActive: boolean;
};
let user: User = {
id: 1,
username: "john_doe",
isActive: true
};
Here, User is a type alias for an object structure that describes a user. This makes it easy to specify that a variable should have this particular structure without rewriting the object type every time.
Type Alias with Optional Properties
Type aliases can include optional properties using the ? symbol.
type Product = {
id: number;
name: string;
price: number;
description?: string; // Optional property
};
let product: Product = {
id: 100,
name: "Desk Lamp",
price: 29.99
// 'description' is optional
};
In this example, the description
property is optional, so product
objects may or may not include it.
Type Alias for Complex Structures
Type aliases can also be used for more complex structures, including objects with methods or nested objects.
type Contact = {
name: string;
email: string;
address: {
street: string;
city: string;
zipCode: string;
};
sendMessage: (message: string) => void;
};
let contact: Contact = {
name: "Alice Johnson",
email: "alice@example.com",
address: {
street: "123 Maple St",
city: "Springfield",
zipCode: "12345"
},
sendMessage: function(message) {
console.log(`Sending message to ${this.email}: ${message}`);
}
};
contact.sendMessage("Hello, Alice!");
This example shows a Contact type with nested object properties and a method, demonstrating how type aliases can encapsulate complex behaviors and structures.
Type Alias for Functions
Type aliases can also be used for functions. This allows you to define a function type with a specific return type and parameter types.
type Point = {
x: number;
y: number;
};
// Takes input variable of type "Point"
function printPoint(point: Point) {
console.log(`The point is at ${point.x}, ${point.y}`);
}
// Return value has the shape of Point
function generateRandomPoint(): Point {
return {
x: Math.random() * 100,
y: Math.random() * 100,
};
}
Unions and Intersections
In TypeScript, unions and intersections are powerful features that allow you to combine types in flexible ways, catering to various scenarios in type definition. They provide a way to handle variables and functions that can operate on more than one type of data.
A union type is a way to declare a variable that can hold values of different types. It is defined using the |
operator, and it signifies that a variable can be any one of several types.
Basic Union Type
type StringOrNumber = string | number;
let identifier: StringOrNumber;
identifier = '123'; // Valid
identifier = 123; // Also valid
Here, identifier
can be either a string
or a number
.
Union of Object Types
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
sideLength: number;
};
type Shape = Circle | Square;
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.sideLength ** 2;
}
}
Shape
is a union type that can be either a Circle
or a Square
, and the function getArea
calculates the area based on the kind of shape.
An intersection type combines multiple types into one. It is defined using the & operator, meaning the resulting type will have all properties of all constituent types.
Basic Intersection
type User = {
name: string;
age: number;
};
type Employee = {
companyId: string;
};
type EmployeeUser = User & Employee;
let eu: EmployeeUser = {
name: "Alice",
age: 28,
companyId: "12345"
};
EmployeeUser
is an intersection type that includes all properties from both User
and Employee
.
Using Intersection to Enhance Functionality
type Point = {
x: number;
y: number;
};
type Point3D = Point & {
z: number;
};
let point3D: Point3D = {
x: 1,
y: 2,
z: 3
};
Point3D
combines properties from Point
with an additional z
coordinate.
To recap:
- Unions: Useful when a value can be one of several types. They are often used in scenarios where a function needs to handle inputs of multiple types.
- Intersections: Useful for combining multiple existing types or enhancing a type with additional properties. They are particularly handy in mixin patterns or to enforce a combination of behaviors.
Null Safety
Null safety is a concept in programming that ensures variables and object references do not unintentionally hold or return null values. Null safety is crucial because accessing properties or methods on null values results in runtime errors, which are one of the most common issues in software development. By ensuring that a variable cannot be null without explicit handling, you can write more robust and error-free code.
All types are non-nullable by default. This means you cannot assign null or undefined to a variable of type number, string, etc., without an explicit assertion.
let name: string = "Alice";
name = null; // Error: Type 'null' is not assignable to type 'string'.
If you want a variable to be able to hold a null value, you must explicitly include null as part of its type using a union type.
let age: number | null = null;
age = 25; // Valid
Optional chaining is a safe way to access deeply nested properties of an object when there’s a possibility that an intermediate property is null or undefined.
type User = {
profile?: {
age: number;
};
};
let user: User = {};
let age = user.profile?.age; // 'age' is 'number | undefined'
Null and Undefined
In TypeScript, null
and undefined
are two primitive types that each represent the absence of a value, but they are used in slightly different contexts. Understanding these types is crucial for managing the absence of data effectively and avoiding common bugs related to uninitialized variables or missing values.
Examples of null:
let firstName: string = "John";
firstName = null; //error, firstName is not nullable
let lastName: string | null = "Smith";
lastName = null; //ok
Example of undefined:
function calculate(): number | undefined {
// Simulate a function that does not always assign a value
if (Math.random() > 0.5) {
return 42;
}
// undefined is returned if random is less than 0.5
}
The key difference is null
is an explicit value you can assign to a variable to represent “no value”. undefined
is the default state of a variable that has not been assigned any value since it was declared. Javascript initializes variables as undefined
by default. null
needs to be explicitly assigned. TypeScript respects these semantics by default. Typescript is a null safe language. That means by default variables cannot be null or undefined unless you explicitly declare them as such.
You can define variables, parameters, and return types as nullable (able to hold null
or undefined
) or non-nullable, thereby making your intent clear and your program’s behavior more predictable.
function process(data: string | null | undefined) {
if (data === null || data === undefined) {
console.log("No data provided.");
} else {
console.log(`Processing: ${data}`);
}
}
Understanding the differences and appropriate uses of null and undefined is vital for writing robust TypeScript programs, especially in managing and validating data presence and state within applications.
Async and Promises
In TypeScript, as in JavaScript, Promise and async functions are fundamental concepts for handling asynchronous operations. They allow you to work with asynchronous code in a way that is cleaner and easier to reason about compared to traditional callback-based approaches.
async
functions simplify the behavior of using promises and allow you to handle asynchronous operations more synchronously, or in a more linear fashion. When you declare a function as async
, it implicitly returns a Promise
.
type User = {
id: number;
name: string;
email: string;
};
async function fetchData(): Promise<User> {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
In this example, fetchData
is an async
function that fetches data from a URL and returns it as JSON. The await keyword is used to pause the execution of the function until the promise settles, and it makes the asynchronous code look and behave a bit more like synchronous code. While data
is a type User
because the function contains the async
keyword, the return type of the function is Promise<User>
.
Typescript and React
Because react components are just javascript functions, typescript can be used to easily type check your component props.
type ButtonProps = {
onClick: () => void;
label: string;
};
function Button({ onClick, label }: ButtonProps) => (
<button onClick={onClick}>{label}</button>
);
Here button take two props, a function called onClick and a string called label. When using the react components if both props are not set typescript will give an error. You should always make sure to clearly define types for your props to avoid any runtime errors.
Conclusion
TypeScript is a powerful superset of JavaScript that adds static typing to the language, enabling developers to catch errors early in the development process and enhancing code readability and maintainability. The ability to explicitly define the shape of objects, function parameters, and return types, allows developers to write more predictable and robust code.
TypeScript has become an indispensable tool for modern web development, particularly in complex projects and those involving large teams or multiple collaborators. Its integration with popular frameworks like React further solidifies its position as a key enabler of enterprise-grade software solutions.