Skip to content

Commit fce926b

Browse files
authored
Infer concrete types from constructor TypeClass forms (#1526)
Support the constructor form of `TypeClass` (the value returned by `rclnodejs.require(...)`) with full type inference for messages, services, and actions, bringing it on par with the existing string and object-descriptor forms, and make the runtime loader accept that form so it works end to end. ## Types - `TypeClass` now recognizes message, service (`{ Request, Response }`), and action (`{ Goal, Feedback, Result }`) constructor shapes (`types/node.d.ts`, `types/service.d.ts`, `types/action_client.d.ts`). - `ServiceRequestMessage` / `ServiceResponseMessage` and `ActionGoal` / `ActionFeedback` / `ActionResult` infer the concrete payload types from the constructor. - `ServiceType` / `ActionType` and the `rostsd_gen` generator (`rostsd_gen/index.js`) handle the constructor form. ## Runtime - `lib/interface_loader.js`: `loadInterface()` accepts an already-loaded interface constructor and returns it as-is (idempotent), validated via the generated class's static `type()` method. Arbitrary functions still fall through to the existing `MESSAGE_NOT_FOUND` error. This fixes the `MESSAGE_NOT_FOUND` failure when a constructor was passed to `ActionServer` / `ActionClient`, which call `loadInterface` unconditionally. ## Tests - `test/types/index.test-d.ts`: `tsd` coverage for every `TypeClass` form — message, service, and action via constructor, string, and object descriptor. - `test/test-action-server.js`, `test/test-service.js`: runtime regression tests that pass the constructor form to `ActionServer` / `ActionClient` and `createService` / `createClient` and exercise a full round-trip. - `test/test-message-type.js`: unit tests for `loadInterface()` idempotency and the arbitrary-function rejection path. ## Demos - `topics`, `services`, and `actions` TypeScript demos updated to demonstrate both the string-name and message/service/action-class styles, with typed callbacks via `typeof Ctor`. - All three demos depend on the local rclnodejs (`file:../../..`) and document `npm install --ignore-scripts`; README source examples use the current LTS (Lyrical). Fix: #1525
1 parent cd5f5b1 commit fce926b

23 files changed

Lines changed: 528 additions & 112 deletions

demo/electron/manipulator/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ npm start
6464
1. **Source your ROS2 environment**:
6565

6666
```bash
67-
source /opt/ros/humble/setup.bash # or your ROS2 installation path
67+
source /opt/ros/$ROS_DISTRO/setup.bash # or your ROS2 installation path
6868
```
6969

7070
2. **Run the demo**:
@@ -140,7 +140,7 @@ To verify the demo is working correctly, you can monitor the published topics:
140140

141141
```bash
142142
# In a separate terminal, source ROS2 environment
143-
source /opt/ros/humble/setup.bash # or your ROS2 installation
143+
source /opt/ros/$ROS_DISTRO/setup.bash # or your ROS2 installation
144144

145145
# List all available topics
146146
ros2 topic list
@@ -199,7 +199,7 @@ Even as a standalone application, **ROS 2 must be installed and sourced on the t
199199

200200
```bash
201201
# Source ROS2 environment
202-
source /opt/ros/humble/setup.bash
202+
source /opt/ros/$ROS_DISTRO/setup.bash
203203

204204
# Run the packaged executable
205205
./out/rclnodejs-manipulator-demo-linux-x64/rclnodejs-manipulator-demo
@@ -301,7 +301,7 @@ manipulator/
301301

302302
```bash
303303
# Source ROS2 environment manually
304-
source /opt/ros/humble/setup.bash # or your ROS2 installation path
304+
source /opt/ros/$ROS_DISTRO/setup.bash # or your ROS2 installation path
305305
npm start
306306
```
307307

demo/typescript/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Asynchronous actions with progress feedback and cancellation
3535
- **Full typing** for all ROS2 messages, services, and actions
3636
- **Compile-time validation** of message structures
3737
- **IntelliSense support** in VS Code and other TypeScript editors
38+
- **Two equivalent forms**: identify a type by its **string name**
39+
(e.g. `'std_msgs/msg/String'`) or by its **class/constructor** obtained from
40+
`rclnodejs.require(...)`. With the class form, TypeScript infers the concrete
41+
message/service/action types automatically\u2014no explicit annotations needed.
3842

3943
### Modern Development
4044

demo/typescript/actions/README.md

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Before running this demo, ensure you have:
6060
1. **Ensure ROS2 is sourced**:
6161

6262
```bash
63-
source /opt/ros/humble/setup.bash # or your ROS2 distribution
63+
source /opt/ros/lyrical/setup.bash # or your ROS2 distribution
6464
```
6565

6666
2. **Build the main rclnodejs package** (if not already done):
@@ -79,8 +79,13 @@ Before running this demo, ensure you have:
7979

8080
4. **Install dependencies**:
8181

82+
This demo depends on the **local rclnodejs** in this repository
83+
(`"rclnodejs": "file:../../.."`). Install with `--ignore-scripts` so npm
84+
links the local package without trying to rebuild its native addon (the
85+
prebuilt binary in the repo is reused):
86+
8287
```bash
83-
npm install
88+
npm install --ignore-scripts
8489
```
8590

8691
5. **Build the TypeScript code**:
@@ -194,29 +199,52 @@ You can also test the action server using ROS2 command-line tools:
194199

195200
## Understanding the Code
196201

202+
Both the server and client show the **two equivalent, fully type-safe ways**
203+
to identify an action type:
204+
205+
- **String name**: pass the type string, e.g. `'test_msgs/action/Fibonacci'`.
206+
- **Action class**: obtain the constructor with `rclnodejs.require(...)` and
207+
pass it directly. TypeScript then infers the goal, feedback and result types
208+
from the constructor, so no explicit `any` annotations are needed.
209+
210+
This demo uses the **action class** form. The constructor is loaded once at
211+
module scope and reused for the type annotations via `typeof Fibonacci`:
212+
213+
```typescript
214+
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
215+
```
216+
197217
### Action Server (`server.ts`)
198218

199-
The action server implements three main callbacks:
219+
The action server implements three main callbacks, all typed from
220+
`typeof Fibonacci`:
200221

201222
1. **Goal Callback**: Decides whether to accept or reject incoming goals
202223

203224
```typescript
204-
goalCallback(goalHandle: any): rclnodejs.GoalResponse {
205-
// Validate the goal and return ACCEPT or REJECT
225+
goalCallback(
226+
goal: rclnodejs.ActionGoal<typeof Fibonacci>
227+
): rclnodejs.GoalResponse {
228+
// Validate the goal (goal.order is typed) and return ACCEPT or REJECT
206229
}
207230
```
208231

209232
2. **Execute Callback**: Performs the actual work (Fibonacci calculation)
210233

211234
```typescript
212-
async executeCallback(goalHandle: any): Promise<any> {
213-
// Calculate Fibonacci sequence and provide feedback
235+
async executeCallback(
236+
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci>
237+
): Promise<rclnodejs.ActionResult<typeof Fibonacci>> {
238+
// Calculate the sequence and publish typed feedback
214239
}
215240
```
216241

217242
3. **Cancel Callback**: Handles goal cancellation requests
243+
218244
```typescript
219-
cancelCallback(goalHandle: any): rclnodejs.CancelResponse {
245+
cancelCallback(
246+
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci> | undefined
247+
): rclnodejs.CancelResponse {
220248
// Return ACCEPT to allow cancellation
221249
}
222250
```
@@ -226,14 +254,23 @@ The action server implements three main callbacks:
226254
The action client:
227255

228256
1. Waits for the action server to be available
229-
2. Creates and sends a goal
230-
3. Handles feedback during execution
257+
2. Creates and sends a goal (`new Fibonacci.Goal()`)
258+
3. Handles typed feedback during execution
231259
4. Processes the final result
232260

233261
```typescript
262+
const goal = new Fibonacci.Goal();
263+
goal.order = FIBONACCI_ORDER;
264+
234265
const goalHandle = await this.actionClient.sendGoal(goal, (feedback) =>
235266
this.feedbackCallback(feedback)
236267
);
268+
269+
private feedbackCallback(
270+
feedback: rclnodejs.ActionFeedback<typeof Fibonacci>
271+
): void {
272+
// feedback.sequence is typed
273+
}
237274
```
238275

239276
## Customization
@@ -263,7 +300,7 @@ You can modify the demo to:
263300

264301
4. **ROS2 environment not sourced**:
265302
```bash
266-
source /opt/ros/humble/setup.bash # or your ROS2 distribution
303+
source /opt/ros/lyrical/setup.bash # or your ROS2 distribution
267304
```
268305

269306
### Debugging

demo/typescript/actions/package.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,7 @@
3333
"ts-node": "^10.9.2",
3434
"typescript": "^5.8.3"
3535
},
36-
"peerDependencies": {
37-
"rclnodejs": "^2.0.0"
38-
},
39-
"peerDependenciesMeta": {
40-
"rclnodejs": {
41-
"optional": false
42-
}
36+
"dependencies": {
37+
"rclnodejs": "file:../../.."
4338
}
4439
}

demo/typescript/actions/src/client.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,44 @@
33
*
44
* This demo shows how to create a ROS2 action client using TypeScript
55
* with rclnodejs. It sends Fibonacci calculation goals to an action server.
6+
*
7+
* It also demonstrates the two equivalent, fully type-safe ways to
8+
* identify an action type:
9+
* - String name: new rclnodejs.ActionClient(node, 'test_msgs/action/Fibonacci', ...)
10+
* - Action class: new rclnodejs.ActionClient(node, Fibonacci, ...)
11+
*
12+
* This demo uses the class form (obtained from `rclnodejs.require(...)`):
13+
* TypeScript infers the goal, feedback and result types directly from the
14+
* constructor, so the feedback callback needs no explicit `any` annotation.
615
*/
716

817
import * as rclnodejs from 'rclnodejs';
918

1019
const ACTION_NAME = 'fibonacci';
1120
const FIBONACCI_ORDER = 10;
1221

22+
// The Fibonacci action class (type-based form). Passing this constructor to
23+
// ActionClient lets TypeScript infer the goal/feedback/result types. The
24+
// string form 'test_msgs/action/Fibonacci' works identically.
25+
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
26+
1327
/**
1428
* Fibonacci Action Client Class
1529
*/
1630
class FibonacciActionClient {
1731
private node: rclnodejs.Node;
18-
private actionClient: rclnodejs.ActionClient<'test_msgs/action/Fibonacci'>;
32+
private actionClient: rclnodejs.ActionClient<typeof Fibonacci>;
1933

2034
constructor(node: rclnodejs.Node) {
2135
this.node = node;
2236

2337
// Start spinning the node to handle callbacks
2438
rclnodejs.spin(node);
2539

26-
// Create action client for Fibonacci action
40+
// Create action client for Fibonacci action using the action class
2741
this.actionClient = new rclnodejs.ActionClient(
2842
node,
29-
'test_msgs/action/Fibonacci',
43+
Fibonacci,
3044
ACTION_NAME
3145
);
3246
}
@@ -41,10 +55,7 @@ class FibonacciActionClient {
4155
await this.actionClient.waitForServer();
4256
this.node.getLogger().info('✓ Action server is available');
4357

44-
// Get the Fibonacci action interface
45-
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
46-
47-
// Create a new goal
58+
// Create a new goal (goal.order is typed from the inferred goal type)
4859
const goal = new Fibonacci.Goal();
4960
goal.order = FIBONACCI_ORDER;
5061

@@ -53,10 +64,9 @@ class FibonacciActionClient {
5364
.info(`Sending goal request for Fibonacci(${goal.order})...`);
5465

5566
try {
56-
// Send the goal with feedback callback
57-
const goalHandle = await this.actionClient.sendGoal(
58-
goal,
59-
(feedback: any) => this.feedbackCallback(feedback)
67+
// Send the goal with feedback callback (feedback is typed)
68+
const goalHandle = await this.actionClient.sendGoal(goal, (feedback) =>
69+
this.feedbackCallback(feedback)
6070
);
6171

6272
if (!goalHandle.isAccepted()) {
@@ -91,7 +101,9 @@ class FibonacciActionClient {
91101
/**
92102
* Callback function for receiving feedback from the action server
93103
*/
94-
private feedbackCallback(feedback: any): void {
104+
private feedbackCallback(
105+
feedback: rclnodejs.ActionFeedback<typeof Fibonacci>
106+
): void {
95107
this.node
96108
.getLogger()
97109
.info(`📊 Received feedback: [${feedback.sequence.join(', ')}]`);

demo/typescript/actions/src/server.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,40 @@
33
*
44
* This demo shows how to create a ROS2 action server using TypeScript
55
* with rclnodejs. It provides a Fibonacci calculation service.
6+
*
7+
* It also demonstrates the two equivalent, fully type-safe ways to
8+
* identify an action type:
9+
* - String name: new rclnodejs.ActionServer(node, 'test_msgs/action/Fibonacci', ...)
10+
* - Action class: new rclnodejs.ActionServer(node, Fibonacci, ...)
11+
*
12+
* This demo uses the class form (obtained from `rclnodejs.require(...)`):
13+
* TypeScript infers the goal, feedback and result types directly from the
14+
* constructor, so the callbacks need no explicit `any` annotations.
615
*/
716

817
import * as rclnodejs from 'rclnodejs';
918

1019
const ACTION_NAME = 'fibonacci';
1120

21+
// The Fibonacci action class (type-based form). Passing this constructor to
22+
// ActionServer lets TypeScript infer the goal/feedback/result types. The
23+
// string form 'test_msgs/action/Fibonacci' works identically.
24+
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
25+
1226
/**
1327
* Fibonacci Action Server Class
1428
*/
1529
class FibonacciActionServer {
1630
private node: rclnodejs.Node;
17-
private actionServer: rclnodejs.ActionServer<'test_msgs/action/Fibonacci'>;
31+
private actionServer: rclnodejs.ActionServer<typeof Fibonacci>;
1832

1933
constructor(node: rclnodejs.Node) {
2034
this.node = node;
2135

22-
// Create action server for Fibonacci action
36+
// Create action server for Fibonacci action using the action class
2337
this.actionServer = new rclnodejs.ActionServer(
2438
node,
25-
'test_msgs/action/Fibonacci',
39+
Fibonacci,
2640
ACTION_NAME,
2741
this.executeCallback.bind(this),
2842
this.goalCallback.bind(this),
@@ -39,13 +53,12 @@ class FibonacciActionServer {
3953
* Execute callback - performs the Fibonacci calculation
4054
*/
4155
async executeCallback(
42-
goalHandle: rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'>
43-
): Promise<any> {
56+
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci>
57+
): Promise<rclnodejs.ActionResult<typeof Fibonacci>> {
4458
this.node
4559
.getLogger()
4660
.info(`🚀 Executing goal for Fibonacci(${goalHandle.request.order})`);
4761

48-
const Fibonacci = rclnodejs.require('test_msgs/action/Fibonacci');
4962
const feedbackMessage = new Fibonacci.Feedback();
5063
const sequence: number[] = [0, 1];
5164

@@ -110,13 +123,12 @@ class FibonacciActionServer {
110123
}
111124

112125
/**
113-
* Goal callback - decides whether to accept or reject incoming goals
114-
*
115-
* Note: According to the type definition, this should receive ActionGoal<T>,
116-
* but the actual implementation may pass different types. Using 'any' to handle
117-
* this inconsistency and support ActionGoal<T>.
126+
* Goal callback - decides whether to accept or reject incoming goals.
127+
* The goal type is inferred from the action constructor.
118128
*/
119-
goalCallback(goal: any): rclnodejs.GoalResponse {
129+
goalCallback(
130+
goal: rclnodejs.ActionGoal<typeof Fibonacci>
131+
): rclnodejs.GoalResponse {
120132
const order = goal.order;
121133

122134
this.node
@@ -146,9 +158,7 @@ class FibonacciActionServer {
146158
* Cancel callback - handles goal cancellation requests
147159
*/
148160
cancelCallback(
149-
goalHandle:
150-
| rclnodejs.ServerGoalHandle<'test_msgs/action/Fibonacci'>
151-
| undefined
161+
goalHandle: rclnodejs.ServerGoalHandle<typeof Fibonacci> | undefined
152162
): rclnodejs.CancelResponse {
153163
this.node.getLogger().info('📥 Received cancel request');
154164
return rclnodejs.CancelResponse.ACCEPT;

0 commit comments

Comments
 (0)