TypeScript functions can technically return only one value, but in real-world projects, we often need to return several pieces of data at once, such as the result of an operation, an error message, and some metadata.
There are multiple ways to model this in TypeScript, and each option comes with different trade-offs for readability, type safety, and maintainability. Choosing the wrong approach can make your code harder to understand and refactor later.
In this tutorial, you’ll learn five practical patterns to return multiple values from a function in TypeScript: arrays, tuples, objects with interfaces, discriminated unions, and classes. For each pattern, you’ll see when to use it, when to avoid it, and how it’s used in real-world scenarios.
Why Return Multiple Values in TypeScript?
In many applications, returning a single primitive or object is not enough. Consider these common scenarios:
- A login function needs to return whether authentication succeeded, the user’s token, and the token expiry time.
- A data fetch function might want to return both data and information about the HTTP status and any error.
- A validation function may need to return the cleaned value and a list of validation messages.
TypeScript provides several patterns to model these cases without compromising type safety. Your job as a developer is to pick the pattern that best fits your use case.
Methods to Return Multiple Values in TypeScript
In this section, we’ll look at five methods:
- Using arrays
- Using tuples
- Using objects and interfaces
- Using discriminated unions for success/error
- Using custom classes
At the end, you’ll find a decision guide that helps you choose the right method for your scenario.
Use Arrays: Best for Ordered, Same-Type Returns
Arrays are the simplest way to return multiple values when the order of values matters, and the values are closely related.
function getMinMax(numbers: number[]): [number, number] {
const min = Math.min(...numbers);
const max = Math.max(...numbers);
return [min, max];
}
const [min, max] = getMinMax([3, 1, 8, 2, 5]);
console.log(`Min: ${min}, Max: ${max}`); // Min: 1, Max: 8You can see the output in the screenshot below.

Here, we return the minimum and maximum values as a pair. We then use array destructuring to access them in a concise way.
When to use arrays
- When values are strongly ordered (like min/max, x/y coordinates).
- When values have the same logical meaning or type (e.g., a list of numbers or strings).
When to avoid arrays
If the values have different semantic meanings (for example, name, age, isActive), an array like [name, age, isActive] is harder to understand than an object, because result[0] or result[1] doesn’t explain what the value represents.
Use Tuples: The TypeScript-Native Pattern
Tuples are a TypeScript feature that extends arrays with fixed length and specific types for each position. This makes them ideal for returning multiple values of different types in a fixed order.
Example: Return Auth Result as a Tuple
type AuthResult = [success: boolean, token: string, expiresIn: number];
function authenticate(username: string, password: string): AuthResult {
if (username === "admin" && password === "secret") {
return [true, "abc123token", 3600];
}
return [false, "", 0];
}
const [isAuthenticated, token, expiresIn] = authenticate("admin", "secret");
if (isAuthenticated) {
console.log(`Token: ${token}, Expires in: ${expiresIn}s`);
}
You can see the output in the screenshot below.

In this example:
- We define a labeled tuple type AuthResult.
- Each element has a specific meaning and type.
- Labeled tuples improve editor hints and readability.
Real-world connection
React’s useState hook is a classic example of this pattern:
const [count, setCount] = useState(0);
It returns a tuple: the current state and a setter function. This is one of the most common patterns for returning multiple values in TypeScript-heavy codebases.
When to use tuples
- When the number of values is fixed.
- When you have mixed types in a specific order.
- When you’re designing hook-like APIs or functional-style utilities.
Tip: controlling inference
Sometimes TypeScript will infer an array type like (string | number)[] instead of a tuple. You can fix this by:
- Declaring an explicit tuple type (as shown above), or
- Using
as constwhen returning a literal:
function getPair() {
return ["left", "right"] as const; // inferred as readonly ["left", "right"]
}Use Objects and Interfaces: The Most Readable Approach
Objects are the most self-descriptive way to return multiple values. Each property has a name that tells you what it represents. Combining objects with interfaces or type aliases gives you strong typing and readability.
Example: Return User Data as an Object
interface UserData {
name: string;
age: number;
isActive: boolean;
}
function getUserData(): UserData {
return {
name: "Jane Smith",
age: 25,
isActive: false,
};
}
const userData = getUserData();
console.log(`Name: ${userData.name}, Age: ${userData.age}, Active: ${userData.isActive}`);This approach clearly describes what each value means and allows TypeScript to catch missing or incorrect properties.
interface vs type
You could also write:
type UserData = {
name: string;
age: number;
isActive: boolean;
};Use interface when:
- You expect to extend the type in other files.
- You want to use declaration merging.
Use type when:
- It’s a one-off shape.
- You’re combining unions or intersection types.
When to use objects
- When you want readable, self-documenting code.
- When the set of returned values might grow over time.
- When you need to pass the result through multiple layers, and readability matters.
For many teams, returning objects with interfaces is the default recommendation.
Use Discriminated Unions: Safe Success and Error Returns
A very common pattern is a function that can either succeed (returning a value) or fail (returning an error). Instead of throwing errors or returning null, you can use a discriminated union to model both cases in a type-safe way.
Example: Result Type for JSON Parsing
type Success<T> = {
ok: true;
value: T;
};
type Failure = {
ok: false;
error: string;
};
type Result<T> = Success<T> | Failure;
function parseJSON(input: string): Result<object> {
try {
const parsed = JSON.parse(input);
return { ok: true, value: parsed };
} catch {
return { ok: false, error: "Invalid JSON string" };
}
}
const result = parseJSON('{"name": "Alice"}');
if (result.ok) {
console.log("Parsed value:", result.value);
} else {
console.error("Error:", result.error);
}You can see the output in the screenshot below.

Here:
Result<T>can be either a success or failure.- The
okproperty acts as the discriminant. - TypeScript automatically narrows the type inside each branch of the if statement.
Why this is powerful
- You force the caller to handle both success and failure.
- You avoid returning loosely typed values like
anyor null. - The types stay in sync as your function evolves.
When to use discriminated unions
- For functions that can fail in expected ways, such as parsing, validation, or network calls.
- When you want to keep your code functional and predictable, without throwing exceptions in normal control flow.
Use Custom Classes: When Your Return Value Needs Behavior
Sometimes you need to return not just data, but also related behavior (methods). In these scenarios, a class can encapsulate both.
Example: User Data with Methods
class UserData {
constructor(
public name: string,
public age: number,
public isActive: boolean
) {}
celebrateBirthday(): void {
this.age += 1;
}
deactivate(): void {
this.isActive = false;
}
}
function getUserData(): UserData {
return new UserData("Michael Brown", 40, true);
}
const userData = getUserData();
userData.celebrateBirthday();
console.log(`Name: ${userData.name}, Age: ${userData.age}, Active: ${userData.isActive}`);In this example:
UserDataencapsulates both data (properties) and operations (methods).- The function returns a fully initialized instance ready to be used.
When to use classes
- When you want to bundle data and behavior together.
- When following an object-oriented design.
- When you need lifecycle methods or want to enforce invariants via constructors.
When not to use classes
- For simple data structures with no behavior.
- When you just need to transport data between layers. In those cases, interfaces and objects are simpler.
Which Method Should You Use? (Quick Decision Guide)
Use this guide when you’re not sure which pattern is appropriate:
| Method | Best For | TypeScript Benefit |
|---|---|---|
| Array | Ordered values with similar meaning | Simple and concise |
| Tuple | Fixed count, mixed types, hook-like APIs | Strong positional typing, editor support |
| Object + Interface/Type | Readable, named values, evolving shapes | Self-documenting, compiler-checked properties |
| Discriminated Union | Success/error results, parse/validate patterns | Safe type narrowing, avoids unchecked any |
| Class | Data plus behavior and methods | Encapsulation, OOP-friendly |
Practical rule of thumb
- If you’re unsure, start with an object + interface.
- Use tuples when you want a compact, hook-style API.
- Use discriminated unions whenever you need to return either a value or an error in a predictable way.
Frequently Asked Questions
Can a TypeScript function return two different types?
Yes. You can model this using a union type or a discriminated union. For example:
type NumberOrString = number | string; function parseNumber(input: string): NumberOrString { const parsed = Number(input); return isNaN(parsed) ? input : parsed; }
However, discriminated unions are usually safer because they enforce explicit handling of each case.
What is the difference between a tuple and an array in TypeScript?
An array like number[] is a list of values of the same type with variable length.
A tuple like [string, number] has a fixed length and specific types at each position.
Use tuples when the position has meaning and you know exactly how many elements there are.
How do I return a value and an error message together?
Use either an object or a discriminated union:
type ValidationResult = {
value: string | null;
error: string | null;
};
function validateEmail(email: string): ValidationResult {
if (!email.includes(“@”)) {
return { value: null, error: “Invalid email address” };
}
return { value: email, error: null };
}
Or use the Result<T> pattern shown earlier for more robust handling.
Can async functions return multiple values in TypeScript?
Yes. The same patterns apply; you just wrap the return type in a Promise:
interface UserWithToken {
userId: string;
token: string;
}
async function loginAsync(): Promise<UserWithToken> {
// simulate async call
return {
userId: “123”,
token: “abc123”,
};
}
The calling code can then await the function and destructure or access the properties as needed.
Conclusion
Returning multiple values in TypeScript is less about syntax and more about choosing the right pattern for your situation. Arrays and tuples are concise and great for ordered values, objects with interfaces are the most readable and flexible, discriminated unions give you safe success/error handling, and classes shine when your return values need behavior as well as data.
By understanding these five patterns and when to use each, you can write TypeScript code that is both expressive and maintainable, and your functions will communicate their intent clearly to anyone reading your code.
You may also read:
- Understanding satisfies vs as Operator in TypeScript
- Loop Through Objects in TypeScript
- Show Alerts in TypeScript
- Merge Objects in TypeScript

Bijay Kumar is an experienced Python and AI professional who enjoys helping developers learn modern technologies through practical tutorials and examples. His expertise includes Python development, Machine Learning, Artificial Intelligence, automation, and data analysis using libraries like Pandas, NumPy, TensorFlow, Matplotlib, SciPy, and Scikit-Learn. At PythonGuides.com, he shares in-depth guides designed for both beginners and experienced developers. More about us.