Generics are a powerful feature of TypeScript. While there’s extensive content available on this topic, this post aims to explore generics more deeply with a niche use case, focusing on advanced applications, conditional types, and other interesting aspects.
This isn’t a typical introduction to generics. Instead, we’ll implement a unique scenario, using it to cover advanced generics, conditional types, and other features.
A Quick Refresher on Generics & Conditional Types
Let’s quickly cover the key concepts of this post with simplified examples.
If you’re already familiar, feel free to skip ahead. If not, this section might serve as a useful refresher.
Generics
Think of generics like function parameters that are types, normally dealing with values (or value references).
“`typescript
function arrayLength(arr: any[]) {
return arr.length;
}
“`
Here, `arr` is an array of `any`. We can type it more accurately using generics.
“`typescript
function arrayLengthTyped(arr: T[]) {
return arr.length;
}
“`
In this case, `T` infers the type of the array’s elements, though the original method was sufficient for counting elements.
Now let’s move to filtering.
“`typescript
function filterUntyped(array: any[], predicate: (item: any) => boolean): any[] {
return array.filter(predicate);
}
“`
This function lacks type checking on the predicate.
“`typescript
type User = { name: string; };
const users: User[] = [];
filterUntyped(users, user => user.nameX === “John”);
“`
Here, generics can ensure type checking.
“`typescript
function filterTyped(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
“`
TypeScript will now catch potential errors.
“`typescript
filterTyped(users, user => user.nameX === “John”);
// —————————–^^^^^
// Property ‘nameX’ does not exist on type ‘User’. Did you mean ‘name’?
“`
Generics can limit arguments, useful for multiple user types.
“`typescript
type AdminUser = User & { role: string; };
type BannedUser = User & { reason: string; };
“`
To filter users correctly:
“`typescript
function filterUserCorrect(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
“`
Conditional Types
These allow for creating types based on type evaluations.
“`typescript
type IsArray = T extends any[] ? true : false;
type YesIsArray = IsArray;
type NoIsNotArray = IsArray;
“`
`YesIsArray` is `true`; `NoIsNotArray` is `false`, illustrating the power of conditional types.
“`typescript
type ArrayOf = T extends Array ? U : never;
type NumberType = ArrayOf;
type NeverType = ArrayOf;
“`
Generic constraints work with helper types.
“`typescript
type ArrayOf2<T extends Array> = T extends Array ? U : never;
type NumberType2 = ArrayOf2;
type NeverType2 = ArrayOf2;
“`
Ensures proper types.
Let’s Get Started
In my two-part series on single flight mutations using TanStack, we created a helper for react-query options that worked seamlessly with server functions, keeping query functions and meta options in sync. Here’s a simplified example.
“`typescript
export function refetchedQueryOptions(queryKey: QueryKey, serverFn: any, arg?: any) {
const queryKeyToUse = […queryKey];
if (arg != null) {
queryKeyToUse.push(arg);
}
return queryOptions({
queryKey: queryKeyToUse,
queryFn: async () => { return serverFn({ data: arg }); },
meta: {
__revalidate: { serverFn, arg },
},
});
}
“`
We aimed to create a fully typed version of `refetchedQueryOptions`. It was challenging yet rewarding.
Our Success Criteria
Here’s our test setup with a partially working setup for refetchedQueryOptions, validating proper type checking.
“`typescript
import { QueryKey, queryOptions } from “@tanstack/react-query”;
import { createServerFn } from “@tanstack/react-start”;
export function refetchedQueryOptions(queryKey: QueryKey, serverFn: any, arg?: any) {
const queryKeyToUse = […queryKey];
if (arg != null) {
queryKeyToUse.push(arg);
}
return queryOptions({
queryKey: queryKeyToUse,
queryFn: async () => { return serverFn({ data: arg }); },
meta:
