Introduction
Have you ever encountered this frustration when developing with Redux Toolkit Query (RTK Query)?
When you want to check the implementation of an API endpoint, pressing F12 (Go to Definition) takes you to the type definition file instead of the actual business logic. You have to manually search for the endpoint name to find its definition in createApi.
This is a common problem because RTK Query hook names (like useGetUserQuery) are dynamically generated, and TypeScript cannot establish a static mapping from hook calls to endpoint definitions.
Today, I'll show you how to develop a TypeScript Language Service Plugin to solve this problem, allowing developers to jump directly to RTK Query endpoint definitions with a single click.
Problem Background
How RTK Query Works
RTK Query creates API slices through createApi:
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUser: builder.query<User, string>({
query: (id) => `/users/${id}`,
}),
updateUser: builder.mutation<User, Partial<User>>({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
}),
}),
})
// Auto-generated hooks
export const { useGetUserQuery, useUpdateUserMutation } = userApi
Pain Points
-
Hook names are dynamically derived:
getUser→useGetUserQuery - TypeScript only sees types: IDE's "Go to Definition" can only point to type gymnastics generated type definitions
- Broken developer experience: Developers need to manually search for endpoint names, interrupting the coding flow
Solution: TypeScript Language Service Plugin
What is a Language Service Plugin?
TypeScript Language Service Plugin is an extension mechanism that allows us to intercept and customize various TypeScript language service operations, including:
- Go to Definition
- Auto Completion
- Hover Information
- Code Refactoring
Core Idea
Our plugin needs to accomplish the following:
-
Identify RTK Query Hooks: Recognize hooks like
use{Endpoint}Query,use{Endpoint}Mutationthrough naming conventions - Parse AST: Find the API instance the hook belongs to
-
Locate Endpoint: Find the corresponding endpoint definition from the API instance's
endpointsproperty - Return Definition Location: Point the jump target to the endpoint definition
Implementation Details
1. Project Structure
rtk-to-endpoints/
├── src/
│ ├── index.ts # Plugin entry
│ └── utils.ts # Core logic
├── package.json
└── tsconfig.json
2. Plugin Entry (index.ts)
import tslib from "typescript/lib/tsserverlibrary";
import { getDefinitionAndBoundSpan } from "./utils.js";
function init(modules: { typescript: typeof tslib }) {
const ts = modules.typescript;
function create(info: tslib.server.PluginCreateInfo) {
const logger = info.project.projectService.logger;
log("✅ Plugin initialized");
const proxy: tslib.LanguageService = Object.create(info.languageService);
// Intercept "Go to Definition" request
proxy.getDefinitionAndBoundSpan = (
fileName: string,
position: number
): tslib.DefinitionInfoAndBoundSpan | undefined => {
const program = info.languageService.getProgram();
// Try our custom jump logic
const definitionInfo = getDefinitionAndBoundSpan(
fileName, position, ts, program
);
// If RTK Query hook is matched, return custom result
// Otherwise, fall back to default behavior
return definitionInfo ||
info.languageService.getDefinitionAndBoundSpan(fileName, position);
};
return proxy;
}
return { create };
}
export = init;
3. Core Logic (utils.ts)
3.1 Recognizing Hook Naming Patterns
RTK Query generated hooks follow fixed naming conventions:
const HOOK_PREFIXES = ["useLazy", "use"] as const;
const HOOK_SUFFIXES = [
"InfiniteQueryState",
"InfiniteQuery",
"QueryState",
"Mutation",
"Query",
] as const;
// Extract endpoint name from hook name
export function extractEndpointName(hookName: string) {
for (const prefix of HOOK_PREFIXES) {
if (hookName.startsWith(prefix)) {
const rest = hookName.slice(prefix.length);
for (const suffix of HOOK_SUFFIXES) {
if (rest.endsWith(suffix)) {
const endpointName = rest.slice(0, rest.length - suffix.length);
if (endpointName) {
// Lowercase first letter: GetUser → getUser
return endpointName[0].toLowerCase() + endpointName.slice(1);
}
}
}
}
}
}
3.2 AST Node Lookup
Use binary search to quickly locate the node at cursor position in the AST:
export function getIdentifierNodeAt(
sourceFile: tslib.SourceFile,
pos: number,
): tslib.Node | undefined {
let current: tslib.Node = sourceFile;
while (true) {
const children = current.getChildren(sourceFile);
let left = 0;
let right = children.length - 1;
let targetChild: tslib.Node | undefined;
// Binary search for child node covering the specified position
while (left <= right) {
const mid = (left + right) >>> 1;
const child = children[mid];
if (pos < child.pos) {
right = mid - 1;
} else if (pos >= child.end) {
left = mid + 1;
} else {
targetChild = child;
break;
}
}
if (!targetChild) break;
current = targetChild;
}
return current;
}
3.3 Finding API Instance
Support two common API usage patterns:
export function findApi(node: tslib.Node, ts: typeof tslib) {
const parent = node.parent;
// Pattern 1: Destructuring assignment
// const { useGetUsersQuery } = userApi
if (ts.isBindingElement(parent)) {
const expressionNode = parent.parent?.parent;
if (!ts.isVariableDeclaration(expressionNode)) return;
const apiNode = expressionNode.getChildAt(
expressionNode.getChildCount() - 1
);
if (!apiNode || !ts.isIdentifier(apiNode)) return;
return apiNode;
// Pattern 2: Property access
// userApi.useGetProductsQuery()
} else if (parent && ts.isPropertyAccessExpression(parent)) {
return parent.getChildAt(parent.getChildCount() - 3);
}
}
3.4 Locating Endpoint Definition
Use TypeScript's type checker to find the target endpoint from the API instance's endpoints property:
export function findEndpoint(
apiNode: tslib.Node,
endpointName: string,
checker: tslib.TypeChecker
) {
// Get the type of API instance
const apiType = checker.getTypeAtLocation(apiNode);
// Get endpoints property
const endpointsSymbol = apiType.getProperty('endpoints');
if (!endpointsSymbol) return;
// Get the type of endpoints
const endpointsType = checker.getTypeOfSymbol(endpointsSymbol);
// Find specific endpoint
const endpointsPropertySymbol = endpointsType.getProperty(endpointName);
return endpointsPropertySymbol;
}
3.5 Assembling Definition Information
export function getDefinitionAndBoundSpan(
fileName: string,
position: number,
ts: typeof tslib,
program?: tslib.Program
) {
const sf = program!.getSourceFile(fileName);
const checker = program!.getTypeChecker();
if (!sf || !program || !checker) return;
// 1. Find identifier node at cursor position
const identNode = getIdentifierNodeAt(sf, position);
if (!identNode || !ts.isIdentifier(identNode)) return;
// 2. Extract endpoint name
const endpointName = extractEndpointName(identNode.getText());
if (!endpointName) return;
// 3. Find API instance
const apiNode = findApi(identNode, ts);
if (!apiNode) return;
// 4. Find endpoint definition
const endpointSymbol = findEndpoint(apiNode, endpointName, checker);
if (!endpointSymbol?.declarations?.length) return;
// 5. Assemble definition information
const definitions = endpointSymbol.declarations.map((node): tslib.DefinitionInfo => {
return {
fileName: node.getSourceFile().fileName,
kind: ts.ScriptElementKind.memberFunctionElement,
name: endpointSymbol.getName(),
containerKind: ts.ScriptElementKind.classElement,
containerName: "endpoints",
textSpan: {
start: node.getStart(),
length: node.getWidth(),
},
};
});
return {
definitions,
textSpan: {
start: identNode.getStart(sf),
length: identNode.getWidth(sf),
},
};
}
Usage
1. Install the Plugin
npm install --save-dev rtk-to-endpoints
2. Configure tsconfig.json
{
"compilerOptions": {
"plugins": [
{
"name": "rtk-to-endpoints"
}
]
}
}
3. Configure VSCode
Since VSCode's built-in TypeScript cannot read npm packages from the project, you need to set VSCode to use the workspace TypeScript version:
-
Ctrl+Shift+P→ Type "TypeScript: Select TypeScript Version" - Select "Use Workspace Version"
- Reload window (
Developer: Reload Window)
Demo
After configuration, when you use "Go to Definition" on any RTK Query hook:
// Click useGetUserQuery to jump directly to getUser endpoint definition
const { data } = userApi.useGetUserQuery(userId);
Before:
- Points to type definition file (no practical business value)
After:
- Directly locates the
getUserendpoint definition increateApi
Technical Summary
1. TypeScript Language Service Architecture
┌─────────────────────────────────────────┐
│ VSCode / IDE │
└─────────────┬───────────────────────────┘
│ LSP Protocol
┌─────────────▼───────────────────────────┐
│ TypeScript Language Server │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ TypeScript Language Service │
│ ┌─────────────────────────────────┐ │
│ │ rtk-to-endpoints Plugin │ │
│ │ (Intercept getDefinition) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
2. Key Technical Points
| Technical Point | Description |
|---|---|
| AST Traversal | Use binary search to efficiently locate nodes |
| Type Checker | Use TypeChecker to parse type information |
| Proxy Pattern | Wrap original Language Service, preserve default behavior |
| Naming Resolution | Identify hook types through string pattern matching |
Extension Ideas
The implementation of this plugin can be extended to other similar scenarios:
-
Vue Composition API: Jump from
useXxxto composable definitions - React Hooks: Enhance custom hook navigation experience
Conclusion
TypeScript Language Service Plugin is a powerful tool that can significantly improve the developer experience. By understanding TypeScript's compiler API and language service architecture, we can build smarter IDE support for specific frameworks and libraries.
I hope this article helps you understand how Language Service Plugins work and inspires you to develop similar tools for your own projects.
References
- TypeScript Wiki: Writing a Language Service Plugin
- Redux Toolkit Query Documentation
- rtk-to-endpoints GitHub
If this article helped you, please like, bookmark, and share!
Feel free to leave comments if you have any questions or suggestions.
Top comments (0)