Introduction
TypeScript is a powerful, statically-typed superset of JavaScript that adds optional static types, interfaces, and generics to the language. This chapter explores the intricacies of TypeScript, including its type system, interfaces, generics, and integration with popular frameworks like React, Vue, and Angular. It also covers advanced TypeScript patterns for writing robust and maintainable code.
Type System, Interfaces, and Generics
Type System
Example 1: Declaring variables with specific types enhances code readability and type safety. For instance, you can declare a variable with a string type:
typescriptlet message: string = "Hello, TypeScript";
Example 2: Using union types allows a variable to hold values of multiple types. This is useful when a variable can have different types of values:
typescriptlet id: number | string;
id = 10; // valid
id = "10"; // valid
Example 3: Defining function return types ensures that functions return the expected type of values. This improves function reliability and predictability:
typescriptfunction add(a: number, b: number): number {
return a + b;
}
Example 4: Type aliases enable you to create custom types. This is helpful for simplifying complex type definitions:
typescripttype StringOrNumber = string | number;
let value: StringOrNumber;
value = "Hello"; // valid
value = 42; // valid
Example 5: Using tuples allows you to define an array with fixed types for each element. This is useful for representing a fixed structure:
typescriptlet person: [string, number];
person = ["John", 25]; // valid
Interfaces
Example 1: Defining an interface for an object ensures that the object adheres to a specific structure. This provides type safety for object properties:
typescriptinterface User {
name: string;
age: number;
}
let user: User = {
name: "Alice",
age: 30
};
Example 2: Extending interfaces allows you to create new interfaces by inheriting properties from existing ones. This promotes code reusability:
typescriptinterface Person {
name: string;
}
interface Employee extends Person {
employeeId: number;
}
let employee: Employee = {
name: "Bob",
employeeId: 123
};
Example 3: Using optional properties in interfaces makes certain properties optional. This is useful for objects that might not always have all properties:
typescriptinterface Product {
name: string;
price?: number;
}
let product: Product = {
name: "Laptop"
};
Example 4: Readonly properties in interfaces prevent modification of properties after they are initialized. This ensures immutability:
typescriptinterface Book {
readonly title: string;
}
let book: Book = {
title: "TypeScript Guide"
};
// book.title = "New Title"; // Error: Cannot assign to 'title' because it is a read-only property
Example 5: Function types in interfaces define the structure of function properties. This is useful for objects that include functions:
typescriptinterface Calculator {
(a: number, b: number): number;
}
let add: Calculator = (a, b) => a + b;
Generics
Example 1: Using generics in functions allows you to create functions that work with different types. This increases code flexibility:
typescriptfunction identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("Hello");
let output2 = identity<number>(42);
Example 2: Generics in interfaces enable you to define interfaces that can be used with different types. This enhances code reusability:
typescriptinterface Box<T> {
contents: T;
}
let stringBox: Box<string> = { contents: "Hello" };
let numberBox: Box<number> = { contents: 42 };
Example 3: Constraining generics ensures that the generic type adheres to a certain structure. This provides additional type safety:
typescriptinterface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength({ length: 10, value: "Hello" }); // valid
Example 4: Using multiple type parameters in generics allows you to create functions or interfaces that work with multiple types. This increases code versatility:
typescriptfunction map<T, U>(array: T[], func: (item: T) => U): U[] {
return array.map(func);
}
let numbers = [1, 2, 3];
let strings = map(numbers, num => num.toString());
Example 5: Generics in classes enable you to create classes that work with different types. This promotes code flexibility and reusability:
typescriptclass GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = addFunction;
}
}
let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
Integrating TypeScript with React, Vue, and Angular
React
Example 1: Creating a functional component in React with TypeScript involves defining the component’s props type. This ensures type safety for component props:
typescriptinterface GreetingProps {
name: string;
}
const Greeting: React.FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}!</h1>;
};
<Greeting name="Alice" />;
Example 2: Using TypeScript with React hooks improves type safety for state and effect hooks. Define the state type and use it with the useState hook:
typescriptconst [count, setCount] = React.useState<number>(0);
Example 3: Creating a custom hook in React with TypeScript involves defining the hook’s return type. This ensures type safety for the hook’s return value:
typescriptfunction useToggle(initialValue: boolean): [boolean, () => void] {
const [value, setValue] = React.useState<boolean>(initialValue);
const toggle = () => setValue(!value);
return [value, toggle];
}
const [isOn, toggle] = useToggle(false);
Example 4: Using TypeScript with React context involves defining the context value type. This ensures type safety for the context value:
typescriptinterface AuthContextType {
isAuthenticated: boolean;
login: () => void;
logout: () => void;
}
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
const authContextValue: AuthContextType = {
isAuthenticated: false,
login: () => {},
logout: () => {}
};
Example 5: Typing higher-order components (HOCs) in React with TypeScript involves defining the wrapped component’s props type. This ensures type safety for the HOC:
typescriptfunction withLogger<T>(WrappedComponent: React.ComponentType<T>): React.FC<T> {
return (props: T) => {
console.log('Props:', props);
return <WrappedComponent {...props} />;
}
}
const LoggedComponent = withLogger(Greeting);
<LoggedComponent name="Alice" />;
Vue
Example 1: Using TypeScript with Vue’s Options API involves defining component props and data types. This ensures type safety for the component’s properties and state:
typescriptimport Vue from 'vue';
interface Props {
name: string;
}
interface Data {
count: number;
}
export default Vue.extend<Props, Data>({
props: {
name: String
},
data() {
return {
count: 0
};
}
});
Example 2: Using TypeScript with Vue’s Composition API involves defining reactive state and computed properties with specific types. This improves type safety:
typescriptimport { defineComponent, ref, computed } from 'vue';
export default defineComponent({
setup() {
const count = ref<number>(0);
const doubled = computed(() => count.value * 2);
return { count, doubled };
}
});
Example 3: Typing Vue methods in TypeScript ensures that method parameters and return values adhere to specific types. This enhances type safety for component methods:
typescriptimport Vue from 'vue';
export default Vue.extend({
methods: {
increment(value: number): number {
return value + 1;
}
}
});
Example 4: Using TypeScript with Vue directives involves defining directive hooks with specific types. This ensures type safety for directive arguments and context:
typescriptimport Vue from 'vue';
Vue.directive('focus', {
inserted(el: HTMLElement) {
el.focus();
}
});
Example 5: Defining TypeScript interfaces for Vue component props and events enhances type safety. This ensures that props and events adhere to specific structures:
typescriptimport Vue from 'vue';
interface Props {
title: string;
}
export default Vue.extend<Props>({
props: {
title: String
},
methods: {
emitEvent() {
this.$emit('customEvent', 'Hello');
}
}
});
Angular
Example 1: Using TypeScript in Angular components involves defining component properties and methods with specific types. This enhances type safety:
typescriptimport { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: '<h1>Hello, {{ name }}!</h1>'
})
export class GreetingComponent {
name: string = 'Alice';
}
Example 2: Typing Angular service methods ensures that method parameters and return values adhere to specific types. This improves type safety for services:
typescriptimport { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
getData(): string[] {
return ['Item1', 'Item2', 'Item3'];
}
}
Example 3: Using TypeScript with Angular forms involves defining form control types. This ensures type safety for form values and validation:
typescriptimport { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-login',
template: `
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="username" />
<input formControlName="password" type="password" />
<button type="submit">Login</button>
</form>
`
})
export class LoginComponent {
loginForm = new FormGroup({
username: new FormControl<string>(''),
password: new FormControl<string>('')
});
onSubmit() {
console.log(this.loginForm.value);
}
}
Example 4: Typing Angular route parameters ensures that route parameters adhere to specific types. This improves type safety for route handling:
typescriptimport { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-user',
template: '<h1>User ID: {{ userId }}</h1>'
})
export class UserComponent implements OnInit {
userId: number;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.userId = Number(this.route.snapshot.paramMap.get('id'));
}
}
Example 5: Using TypeScript with Angular pipes involves defining pipe transformation methods with specific types. This ensures type safety for pipe inputs and outputs:
typescriptimport { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalize'
})
export class CapitalizePipe implements PipeTransform {
transform(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
}
Advanced TypeScript Patterns
Advanced Patterns
Example 1: Using discriminated unions allows you to define types that can take on several different forms, distinguished by a common property. This is useful for complex type definitions:
typescriptinterface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case 'square':
return shape.size * shape.size;
case 'rectangle':
return shape.width * shape.height;
}
}
Example 2: Conditional types allow you to create types based on conditions. This is useful for creating flexible type definitions:
typescripttype MessageOf<T> = T extends { message: unknown } ? T['message'] : never;
interface Email {
message: string;
}
interface SMS {
text: string;
}
type EmailMessageContents = MessageOf<Email>; // string
type SMSMessageContents = MessageOf<SMS>; // never
Example 3: Mapped types allow you to create new types by transforming properties of an existing type. This is useful for creating variations of types:
typescripttype Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
Example 4: Type guards are functions that help narrow down types using conditional checks. This improves type safety in conditional logic:
typescriptfunction isString(value: any): value is string {
return typeof value === 'string';
}
function print(value: string | number) {
if (isString(value)) {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
Example 5: Utility types provided by TypeScript, such as Partial, Omit, and Pick, help manipulate existing types. This simplifies type transformations:
typescriptinterface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
type UserWithoutEmail = Omit<User, 'email'>;
type UserNameAndAge = Pick<User, 'name' | 'age'>;
Conclusion
TypeScript provides powerful tools for writing robust and maintainable code through its type system, interfaces, and generics. Integrating TypeScript with popular frameworks like React, Vue, and Angular enhances type safety and development efficiency. Advanced TypeScript patterns further extend the language’s capabilities, enabling developers to write flexible and concise code. This chapter covered essential TypeScript concepts with practical examples to help you effectively utilize TypeScript in your projects.
Leave a Reply