TypeScript enhances JavaScript with type safety, making it easier to write, read, and maintain code. Functions, a fundamental building block of any program, benefit greatly from TypeScript’s type annotations. This tutorial will guide you through creating typed functions, handling rest parameters, and using function overloading in TypeScript.
Use Functions in TypeScript
Basic Functions with Type Annotations
In TypeScript, you can add type annotations to function parameters and return types to enforce type safety. Let’s start by creating a simple function with type annotations:
function add(a: number, b: number): number {
return a + b;
}
- Parameters: a and b are explicitly defined as number.
- Return Type: The return type number ensures the function always returns a numeric value.
When you try to pass arguments of incorrect types, TypeScript will throw an error during development, reducing runtime errors.
console.log(add(2, 3)); // Correct
console.log(add(2, "3")); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
Optional Parameters
Optional parameters in TypeScript allow functions to accept arguments that may or may not be provided. You define them by adding a ? after the parameter name. This feature helps build flexible APIs while maintaining type safety.
Key Points:
- Syntax: parameterName?: type
- Default Value: If an optional parameter is omitted, its value is undefined.
- Order: Optional parameters must follow required parameters in the function signature to avoid ambiguity.
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}
console.log(greet("Alice")); // Output: Hello, Alice!
console.log(greet("Alice", "Good day")); // Output: Good day, Alice!
Here, greeting is optional, and the function gracefully handles its absence.
Default Parameters
Default parameters are similar to optional parameters but allow you to specify a default value directly in the function signature. If the caller does not provide a value for a parameter, the default value is used.
Key Points:
- Syntax: parameterName: type = defaultValue
- Order: Default parameters can be mixed with required parameters but should ideally come last.
- Evaluation: Default values can be simple constants or the result of a function call.
function multiply(a: number, b: number = 1): number {
return a * b;
}
console.log(multiply(5)); // Output: 5
console.log(multiply(5, 2)); // Output: 10
Default parameters eliminate the need for null checks or explicit handling for missing values.
Rest Parameters
Rest parameters allow functions to accept a variable number of arguments and handle them as an array. They’re defined using the spread operator (...
).
Key Points:
- Type: Rest parameters must always have an array type (type[]).
- Usage: Ideal for scenarios where the number of inputs is unknown or variable.
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // Output: 15
- The rest parameter ...numbers collects all arguments into an array of number.
- The reduce method processes the array and returns the sum.
Anonymous Functions and Arrow Functions
Anonymous functions are unnamed functions often assigned to variables or passed as arguments. Arrow functions (=>) are a concise syntax for writing anonymous functions and are especially useful in callbacks or functional programming.
Anonymous Function:
const divide = function (a: number, b: number): number {
return a / b;
};
console.log(divide(10, 2)); // Output: 5
Arrow Function:
const subtract = (a: number, b: number): number => a - b;
console.log(subtract(10, 5)); // Output: 5
These syntaxes are compact and commonly used in modern TypeScript applications.
Function Overloading
Function overloading in TypeScript lets you define multiple signatures for a function. The function implementation must accommodate all overloads and use type guards or logic to differentiate between them.
Key Points:
- Multiple Signatures: Define the possible combinations of arguments and their types.
- Implementation: Use a single function implementation that handles all cases.
- Type Guards: Employ checks like typeof to determine the argument types during execution.
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}
console.log(format("hello")); // Output: HELLO
console.log(format(42)); // Output: 42.00
- The overloads define possible parameter types.
- The implementation uses a union type (string | number) and includes type guards to differentiate between them.
Typed Callbacks
Typed callbacks are functions passed as arguments to other functions, with strict type definitions ensuring compatibility. This is especially useful for higher-order functions like map, filter, or reduce.
Key Points:
- Type Signature: Specify the input and output types of the callback function.
- Benefits: Prevents runtime errors by catching type mismatches during development.
function processNumbers(
numbers: number[],
callback: (num: number) => number
): number[] {
return numbers.map(callback);
}
const doubled = processNumbers([1, 2, 3], (num) => num * 2);
console.log(doubled); // Output: [2, 4, 6]
Here:
- callback is typed as a function that takes a number and returns a number.
- TypeScript ensures the callback passed to processNumbers matches the expected type.
Higher-Order Functions
Higher-order functions return other functions or accept functions as arguments. Type annotations ensure type safety throughout the chain.
function createMultiplier(factor: number): (value: number) => number {
return (value: number) => value * factor;
}
const triple = createMultiplier(3);
console.log(triple(10)); // Output: 30
Here:
- createMultiplier returns a function with a specific type signature.
- The returned function maintains type safety.
Advanced Usage: Generic Functions
Generics allow functions to work with multiple types while maintaining type safety.
function identity<T>(value: T): T {
return value;
}
console.log(identity<number>(42)); // Output: 42
console.log(identity<string>("Hello")); // Output: Hello
- T is a generic type that adapts to the type of the argument passed.
- This makes the identity function flexible and reusable.
Advanced Usage of Functions in TypeScript
TypeScript provides several advanced features for creating and managing functions, enabling developers to write clean, efficient, and reusable code. Below are some advanced use cases and techniques for using functions in TypeScript.
1. Generic Functions
Generic functions allow you to create functions that work with a variety of types without sacrificing type safety. Generics are particularly useful for scenarios where the type is determined dynamically or depends on the inputs.
Example: Generic Identity Function
function identity<T>(value: T): T {
return value;
}
console.log(identity<number>(42)); // Output: 42
console.log(identity<string>("TypeScript")); // Output: TypeScript
Explanation:
- <T> is a type parameter that acts as a placeholder for a specific type.
- The type is inferred or explicitly provided when the function is called.
Generic Constraints:
You can constrain generics using extends to restrict the types a generic can accept.
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // Output: 5
logLength([1, 2, 3]); // Output: 3
2. Higher-Order Functions
Higher-order functions are functions that take other functions as arguments or return functions. These are commonly used for callbacks, middleware, or functional programming.
Example: Function Returning a Function
function multiplier(factor: number): (value: number) => number {
return (value: number) => value * factor;
}
const double = multiplier(2);
console.log(double(5)); // Output: 10
Example: Function Accepting a Callback
function processNumbers(
numbers: number[],
callback: (value: number) => number
): number[] {
return numbers.map(callback);
}
const squaredNumbers = processNumbers([1, 2, 3], (num) => num * num);
console.log(squaredNumbers); // Output: [1, 4, 9]
3. Recursive Functions
Recursive functions are functions that call themselves to solve problems that can be divided into similar subproblems.
Example: Calculating Factorial
function factorial(n: number): number {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // Output: 120
Tail-Call Optimization
TypeScript supports tail-recursive functions where the recursive call is the last operation, making them memory-efficient.
function tailFactorial(n: number, accumulator: number = 1): number {
if (n <= 1) return accumulator;
return tailFactorial(n - 1, n * accumulator);
}
console.log(tailFactorial(5)); // Output: 120
4. Currying Functions
Currying is the process of transforming a function with multiple arguments into a series of functions that each take a single argument.
Example:
function add(a: number): (b: number) => number {
return (b: number) => a + b;
}
const addFive = add(5);
console.log(addFive(3)); // Output: 8
Currying is useful for creating reusable and composable function pipelines.
5. Function Types
TypeScript allows you to define reusable function types to enforce consistency across multiple functions.
Example:
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;
console.log(add(2, 3)); // Output: 5
console.log(multiply(2, 3)); // Output: 6
6. Callable and Constructable Types
TypeScript allows you to define types for objects that can be called as functions or used as constructors.
Callable Types:
type GreetFunction = {
(name: string): string;
};
const greet: GreetFunction = (name) => `Hello, ${name}!`;
console.log(greet("Alice")); // Output: Hello, Alice!
Constructable Types:
type PersonConstructor = {
new (name: string, age: number): { name: string; age: number };
};
const Person: PersonConstructor = class {
constructor(public name: string, public age: number) {}
};
const john = new Person("John", 30);
console.log(john); // Output: { name: "John", age: 30 }
7. Intersection and Union Types with Functions
You can use intersection and union types to create flexible function signatures.
Example: Union Type
function format(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase();
}
return input.toString();
}
console.log(format("hello")); // Output: HELLO
console.log(format(123)); // Output: 123
Example: Intersection Type
type Logger = (message: string) => void;
type ErrorLogger = Logger & { logError: (error: Error) => void };
const errorLogger: ErrorLogger = Object.assign(
(message: string) => console.log(message),
{
logError: (error: Error) => console.error(error.message),
}
);
errorLogger("This is a log.");
errorLogger.logError(new Error("This is an error."));
8. Asynchronous Functions with async/await
TypeScript supports asynchronous functions with strict type definitions for promises.
Example:
async function fetchData(url: string): Promise<string> {
const response = await fetch(url);
return response.text();
}
fetchData("https://api.example.com").then((data) => console.log(data));
With async/await
, you can write asynchronous code that is easier to read and debug.
9. Custom Utility Functions with keyof and infer
TypeScript's advanced type utilities enable you to create functions that operate on object keys or infer types dynamically.
Example: Function with keyof
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 25 };
console.log(pluck(user, "name")); // Output: Alice
Example: Infer Return Type
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
function sum(a: number, b: number): number {
return a + b;
}
type SumReturnType = ReturnTypeOf<typeof sum>; // Resolves to `number`
10. Decorator Functions
TypeScript decorators can modify or extend the behavior of functions or classes.
Example: Method Decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Called ${propertyKey} with args:`, args);
return originalMethod.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
console.log(calculator.add(2, 3)); // Logs method call and output: 5
Conclusion
TypeScript provides robust tools for creating and working with functions. By leveraging type annotations, optional and default parameters, rest parameters, and function overloading, you can write safer and more maintainable code. Advanced features like generics and typed callbacks further enhance flexibility while maintaining type safety. Mastering these concepts will make your TypeScript functions powerful and versatile.
Advanced functions in TypeScript unlock powerful patterns for writing robust and reusable code. By combining features like generics, function types, decorators, and async/await, you can handle complex scenarios while maintaining type safety and clarity.
Checkout our instant dedicated servers and Instant KVM VPS plans.