Generic Types

Learn how to create flexible, reusable types with TypeScript generics

Updated:
TypeScript UniversityTypeScriptGenerics

The concept of composition, or the idea that “I want to use this same piece of logic in multiple places,” is the foundation of software design and development. This usually manifests as methods or functions that rely on an input and can perform logic or generate an expected output.

You can think of generic types as a special type that can accept a type as an input and return an output based on the input type. An example would be a shared APIResult type.

type APIResponse<TStatusCode, TData> = TStatusCode extends 200
? APISuccessResult<TData>
: APIErrorResult<TStatusCode>;

Don’t let the syntax scare you away. I’ll break down each piece soon. We’ll start by defining a shared mock Product table for demo purposes.

type
type Product = {
id: string;
price: number;
name: string;
categories: string[];
}
Product
= {
id: string
id
: string;
price: number
price
: number;
name: string
name
: string;
categories: string[]
categories
: string[];
};
type
type GetProductsReturn = {
data: Product[];
page: number;
next_page: number | null;
status: 200;
}
GetProductsReturn
= {
data: Product[]
data
:
type Product = {
id: string;
price: number;
name: string;
categories: string[];
}
Product
[];
page: number
page
: number;
next_page: number | null
next_page
: number | null;
status: 200
status
: 200;
};
type
type GetProducts = () => GetProductsReturn
GetProducts
= () =>
type GetProductsReturn = {
data: Product[];
page: number;
next_page: number | null;
status: 200;
}
GetProductsReturn
;
type
type Order = {
id: string;
product_id: string;
quantity: number;
}
Order
= {
id: string
id
: string;
product_id: string
product_id
: string;
quantity: number
quantity
: number;
};
type
type GetOrdersForUserReturn = {
data: Order[];
page: number;
next_page: number | null;
status: 200;
}
GetOrdersForUserReturn
= {
data: Order[]
data
:
type Order = {
id: string;
product_id: string;
quantity: number;
}
Order
[];
page: number
page
: number;
next_page: number | null
next_page
: number | null;
status: 200
status
: 200;
};
type
type GetOrdersForUser = (userId: string) => GetOrdersForUserReturn
GetOrdersForUser
= (
userId: string
userId
: string) =>
type GetOrdersForUserReturn = {
data: Order[];
page: number;
next_page: number | null;
status: 200;
}
GetOrdersForUserReturn
;

These declarations work great for these simple examples, but they can be a burden to maintain. For example, if you look closely the next_page property for each return type is number | null. This is a typo. Each next_page should be a URL of type string | null. Now imagine having to maintain more than two tables.

For this reason, we want to define what a successful API response is shaped like.

type APISuccessResult<T> = {
data: T;
page: number;
next_page: string | null;
status: 200;
};

The T is the input type. So if a consumer passes in a string, then APISuccessResult["data"] is of type string.

type
type Product = {
id: string;
price: number;
name: string;
categories: string[];
}
Product
= {
id: string
id
: string;
price: number
price
: number;
name: string
name
: string;
categories: string[]
categories
: string[];
};
type
type APISuccessResult<T> = {
data: T;
page: number;
next_page: string | null;
status: 200;
}
APISuccessResult
<
function (type parameter) T in type APISuccessResult<T>
T
> = {
data: T
data
:
function (type parameter) T in type APISuccessResult<T>
T
;
page: number
page
: number;
next_page: string | null
next_page
: string | null;
status: 200
status
: 200;
};
type
type ProductSearchApiResult = {
data: Product;
page: number;
next_page: string | null;
status: 200;
}
ProductSearchApiResult
=
type APISuccessResult<T> = {
data: T;
page: number;
next_page: string | null;
status: 200;
}
APISuccessResult
<
type Product = {
id: string;
price: number;
name: string;
categories: string[];
}
Product
>;

We can also constrain the input type by using the extends keyword.

type
type APIErrorResult<T extends 400 | 500> = {
error: {
message: string;
};
status: T;
}
APIErrorResult
<
function (type parameter) T in type APIErrorResult<T extends 400 | 500>
T
extends 400 | 500> = {
error: {
message: string;
}
error
: {
message: string
message
: string;
};
status: T extends 400 | 500
status
:
function (type parameter) T in type APIErrorResult<T extends 400 | 500>
T
;
};
type
type ValidProductSearchApiResult = {
error: {
message: string;
};
status: 400;
}
ValidProductSearchApiResult
=
type APIErrorResult<T extends 400 | 500> = {
error: {
message: string;
};
status: T;
}
APIErrorResult
<400>;
type
type InvalidProductSearchApiResult = {
error: {
message: string;
};
status: 200;
}
InvalidProductSearchApiResult
=
type APIErrorResult<T extends 400 | 500> = {
error: {
message: string;
};
status: T;
}
APIErrorResult
<200>;
Error ts(2344) ― Type '200' does not satisfy the constraint '400 | 500'.