I have found this fantastic fastify plugin (https://github.com/Coobaha/typed-fastify) that uses the following syntax to infer the correct types passed to the Service, given a schema of type T.
import {Schema, Service} from '@coobaha/typed-fastify';
interface ExampleSchema extends Schema {
paths: {
'GET /test/:testId': {
request: {
params: {
testId: {
type: 'string',
}
};
};
// other properties, not relevant for question
};
};
}
const exampleService: Service<ExampleSchema> = {
'GET /test/:testId': (req, reply) => {
const testId = req.params.testId // this passes ✅
const testName = req.params.testName // this fails 💔
// rest of handler
}
};
// example is simplified. Find a full example here: https://github.com/Coobaha/typed-fastify
This plugin is already awesome, but I want to take it even further.
Given a /test/:testId
string, we already know which parameters that URL needs. Having to specify it twice (in the URL and the params
object), it seems to me something worth trying to automate in order to guarantee they always stay in sync.
I would love to find a way to automatically add the params
type, computing it from the key string (the endpoint URL). For example, the key /test/:testId
should have a params
of type {testId: string}
.
Imagine doing:
import {Schema, Service} from '@coobaha/typed-fastify';
interface ExampleSchema extends Schema {
paths: {
'GET /test/:testId': {
// only other properties, no need for explicit params
};
};
}
// and still have the same type-checks on the params 🤯:
const exampleService: Service<ExampleSchema> = {
'GET /test/:testId': (req, reply) => {
const testId = req.params.testId // this passes ✅
const testName = req.params.testName // this fails 💔
// rest of handler
}
};
In my understanding, this would be impossible with default typescript features and possibly only with a plugin to use at compile time using ttypescript.
Is there any way I can avoid using alternative compilers and writing my own plugins?
Thanks for any support
You can achieve this through template literal types introduced in TypeScript 4.1, though for long paths with many parameters, you'll need TypeScript 4.5 for its detection of tail recursive types: Playground
type ExtractParams<T extends string, Acc = {}> =
T extends `${infer _}:${infer P}/${infer R}` ? ExtractParams<R, Acc & { [_ in P]: string }> :
T extends `${infer _}:${infer P}` ? Acc & { [_ in P]: string } :
Acc
type Service<Schema extends BaseSchema> = {
[K in keyof Schema['paths'] & string]: (req: Req<ExtractParams<K>>, reply: Reply) => void;
}
const exampleService: Service<ExampleSchema> = {
'GET /test/:testId': (req, reply) => {
const testId = req.params.testId // this passes ✅
const testName = req.params.testName // this fails ✅
}
};
The magic is in the ExtractParams
type. This works by trying two patterns. First, it checks for *:(param)/(rest)
. If this matches, it adds the inferred param
to the accumulator and recurses on rest
. This catches parameters in the middle of a query (e.g. GET /test/:HERE/later
). Then, it checks for *:(param)
to detect parameters at the end (e.g. GET /test/:testId
). If neither of these match, then processing has finished and it returns the accumulator, with any inferred parameters.