TypeScript Generics are awesome. They allow us to provide more flexible functions that can accept parameters of any type, while still maintaining strict typing that would not come from the any keyword.
This post is not about Generics on their own. If you do not have experience with them, you should consult the Generics documentation first.
An issue with Generics is that sometimes we know the rough shape of the data that we’re working with, for example, a function argument that will surely have a length property. In this case, a normal Generic might cause the compiler to complain:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length); // Property 'length' does not exist on type 'Type'.
return arg;
}As per the documentation, this issue can be solved with Generic Constraints, where you provide TypeScript with more details about the Generic. For the example above, we can tell TypeScript that our Generic Type actually extends another interface:
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}Working with JSDocs
Because I’m often copying code from one codebase to another to the browser to another runtime, etc., I prefer writing my type annotations with JSDocs. However, I sometimes run into scenarios with complex types where I don’t know how what the equivalent syntax is for TS in JSDocs, and nowhere on the internet can I find it.
For TypeScript Generic Constraints in JSDocs, now I can!
The same solution we had with Generic Constraints above can be achieved in JSDocs like so:
/**
* @typedef {{
* length: number
* }} Lengthwise
*/
/**
* @template {Lengthwise} Type
* @param {Type} arg
* @returns {Type}
*/
function loggingIdentity(arg) {
console.log(arg.length); // Also no complaints
return arg;
} We can create a Generic in JSDocs using the @template keyword, and by providing it with a type definition in the curly brackets, we get a Generic Constraint.
The secret is @template {Lengthwise} Type, where @template tells the compiler you are creating a Generic, {Lengthwise} provides the Constraint, and Type (in this case) is the name of the Generic. It can be whatever you like. Often in TypeScript files you will just see T.
Real World Example
I’ve been using Drizzle ORM in my applications lately. It’s awesome!!! They have great documentation that includes good, relevant examples, like how to build dynamic queries.
In their examples, they include a utility function that can extend a normal query to add pagination:
function withPagination<T extends PgSelect>(
qb: T,
page: number = 1,
pageSize: number = 10,
) {
return qb.limit(pageSize).offset((page - 1) * pageSize);
}The project I’m working on uses Drizzle in a JavaScript file and while I could rewrite it in TyeScript, I opted to learn how to accomplish the same thing with JSDocs and write about it instead.
/**
* @template {SQLiteSelect} T
* @param {T} qb
* @param {number} page
* @param {number} perPage
*/
export function withPagination(qb, page = 1, perPage = 10) {
return qb.limit(perPage).offset((page - 1) * perPage);
}(My example uses SQLiteSelect instead of PgSelect, but you get the idea)
Hope you found this helpful. If you did, please let me know in the comments, share, or do whatever feels right.
Thanks for stopping by my corner of the internet. Here, there are no ads, trackers, paywalls, accounts, etc. It's hard, but worth the better experience. If you'd like to keep this site up, consider hiring me, making a donation, buying something (soon), leaving a comment, or sharing. It helps more than you know. I'm glad you're :)