Skip to content

Commit a04c6d2

Browse files
author
Teo Koon Peng
authored
better typescript typings (#551)
This commit makes the changes below: * generate type class strings for messages and services * narrow down `TypeClass` type from `string` to known values * generate typings for *Wrapper javascript classes * generate constants as 'const enum' These provide intellisense for typeclass names and allow constants to be referenced by value. The constants are generated with '_Constants' suffix, since ros does not allow underscores in the message name, this should be safe from any conflicts. This is also same as how the ros idl generator exports the constants. Fix #550
1 parent db2a685 commit a04c6d2

File tree

3 files changed

+167
-116
lines changed

3 files changed

+167
-116
lines changed

rostsd_gen/index.js

Lines changed: 102 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function generateAll() {
3838
// load pkg and interface info (msgs and srvs)
3939
const generatedPath = path.join(__dirname, '../generated/');
4040
const pkgInfos = getPkgInfos(generatedPath);
41-
41+
4242
// write message.d.ts file
4343
const messagesFilePath = path.join(__dirname, '../types/interfaces.d.ts');
4444
const fd = fs.openSync(messagesFilePath, 'w');
@@ -56,17 +56,19 @@ function getPkgInfos(generatedRoot) {
5656

5757
const pkgInfo = {
5858
name: pkg,
59-
messages: []
59+
messages: [],
60+
services: []
6061
};
6162

6263
const pkgPath = path.join(rootDir, pkg);
63-
const files = fs.readdirSync(pkgPath);
64+
const files = fs.readdirSync(pkgPath).filter(fn => fn.endsWith('.js'));
6465

6566
for (let filename of files) {
6667
const typeClass = fileName2Typeclass(filename);
6768

6869
if (typeClass.type === 'srv') { // skip __srv__<action>
6970
if (!typeClass.name.endsWith('Request') && !typeClass.name.endsWith('Response')) {
71+
pkgInfo.services.push(typeClass);
7072
continue;
7173
}
7274
}
@@ -93,10 +95,12 @@ function getPkgInfos(generatedRoot) {
9395

9496

9597
function savePkgInfoAsTSD(pkgInfos, fd) {
96-
97-
let fullMessageNames = ['string'];
98+
let messagesMap = {
99+
string: 'string',
100+
};
98101

99102
fs.writeSync(fd, '/* eslint-disable camelcase */\n');
103+
fs.writeSync(fd, '/* eslint-disable max-len */\n');
100104
fs.writeSync(fd, '// DO NOT EDIT\n');
101105
fs.writeSync(fd, '// This file is generated by the rostsd_gen script\n\n');
102106

@@ -115,7 +119,7 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
115119
for (const msgInfo of pkgInfo.messages) {
116120

117121
if (msgInfo.typeClass.type != curNS) {
118-
if (curNS) { // close current ns
122+
if (curNS) { // close current ns
119123
fs.writeSync(fd, ' }\n');
120124
}
121125

@@ -129,10 +133,11 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
129133
}
130134

131135
saveMsgInfoAsTSD(msgInfo, fd);
136+
saveMsgWrapperAsTSD(msgInfo, fd);
132137

133138
// full path to this msg
134-
const fullMessageName = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`;
135-
fullMessageNames.push(fullMessageName);
139+
const fullMessageName = `${pkgInfo.name}/${msgInfo.typeClass.type}/${msgInfo.typeClass.name}`;
140+
messagesMap[fullMessageName] = `${pkgInfo.name}.${msgInfo.typeClass.type}.${msgInfo.typeClass.name}`;
136141
}
137142

138143
if (curNS) {
@@ -144,66 +149,112 @@ function savePkgInfoAsTSD(pkgInfos, fd) {
144149
fs.writeSync(fd, ' }\n\n');
145150
}
146151

147-
// write type alias for Message
148-
// e.g. type Message =
149-
// string |
150-
// std_msgs.msg.Bool |
151-
// std_msgs.msg.Byte |
152-
// ...
153-
fs.writeSync(fd, ' type Message = \n');
154-
for (let i=0; i < fullMessageNames.length; i++) {
155-
fs.writeSync(fd, ' ' + fullMessageNames[i]);
156-
if (i != fullMessageNames.length-1) {
157-
fs.writeSync(fd, ' |\n');
158-
}
152+
// write messages type mappings
153+
fs.writeSync(fd, ' type MessagesMap = {\n');
154+
for (const key in messagesMap) {
155+
fs.writeSync(fd, ` '${key}': ${messagesMap[key]},\n`);
156+
}
157+
fs.writeSync(fd, ' };\n');
158+
fs.writeSync(fd, ' type MessageTypeClassName = keyof MessagesMap;\n');
159+
fs.writeSync(fd, ' type Message = MessagesMap[MessageTypeClassName];\n');
160+
fs.writeSync(fd, ' type MessageType<T> = T extends MessageTypeClassName ? MessagesMap[T] : object;\n\n');
161+
162+
// write message wrappers mappings
163+
fs.writeSync(fd, ' type MessageTypeClassWrappersMap = {\n');
164+
for (const key in messagesMap) {
165+
if (key === 'string') {
166+
fs.writeSync(fd, " 'string': never,\n");
167+
continue;
168+
}
169+
fs.writeSync(fd, ` '${key}': ${messagesMap[key]}_WrapperType,\n`);
170+
}
171+
fs.writeSync(fd, ' };\n');
172+
fs.writeSync(fd, ' type MessageWrapperType<T> = T extends MessageTypeClassName ? MessageTypeClassWrappersMap[T] : object;\n\n');
173+
174+
// write service type class string
175+
const services = [];
176+
for (const pkg of pkgInfos) {
177+
services.push(...pkg.services);
178+
}
179+
if (!services.length) {
180+
fs.writeSync(fd, ' type ServiceTypeClassName = never;\n\n');
181+
} else {
182+
fs.writeSync(fd, ' type ServiceTypeClassName = \n');
183+
for (let i = 0; i < services.length; i++) {
184+
const srv = services[i];
185+
const srvTypeClassStr = `${srv.package}/${srv.type}/${srv.name}`;
186+
fs.writeSync(fd, ` '${srvTypeClassStr}'`);
187+
188+
if (i !== services.length - 1) {
189+
fs.writeSync(fd, ' |\n');
190+
}
191+
}
192+
fs.writeSync(fd, ';\n\n');
159193
}
160-
161-
fs.writeSync(fd, ';\n');
194+
195+
fs.writeSync(fd, ' type TypeClassName = MessageTypeClassName | ServiceTypeClassName;\n');
162196

163197
// close module declare
164198
fs.writeSync(fd, '}\n');
199+
165200
fs.closeSync(fd);
166201
}
167202

168203

169-
function saveMsgInfoAsTSD(msgInfo, fd) {
170-
171-
// write type = xxxx {
172-
const typeTemplate =
173-
` export type ${msgInfo.typeClass.name} = {\n`;
174-
175-
fs.writeSync(fd, typeTemplate);
176-
177-
// write constant definitions
178-
for (let i = 0; i < msgInfo.def.constants.length; i++) {
179-
const constant = msgInfo.def.constants[i];
204+
function saveMsgWrapperAsTSD(msgInfo, fd) {
205+
const msgName = msgInfo.typeClass.name;
206+
fs.writeSync(fd, ` export type ${msgName}_WrapperType = {\n`);
207+
for (const constant of msgInfo.def.constants) {
180208
const constantType = primitiveType2JSName(constant.type);
181-
const tmpl = (constantType == 'string') ?
182-
` ${constant.name}: '${constant.value}'` :
183-
` ${constant.name}: ${constant.value}`;
184-
fs.writeSync(fd, tmpl);
185-
186-
if (i != msgInfo.def.constants.length - 1) {
187-
fs.writeSync(fd, ',\n');
188-
} else if (msgInfo.def.fields.length > 0) {
189-
fs.writeSync(fd, ',\n');
190-
}
209+
fs.writeSync(fd, ` readonly ${constant.name}: ${constantType},\n`);
191210
}
211+
fs.writeSync(fd, ` new(other?: ${msgName}): ${msgName},\n`);
212+
fs.writeSync(fd, ' }\n');
213+
}
192214

193-
// write field definitions
215+
216+
/**
217+
* Writes the message fields as typescript definitions.
218+
*
219+
* @param {*} msgInfo ros message info
220+
* @param {*} fd file descriptor
221+
* @param {string} indent The amount of indent, in spaces
222+
* @param {string} lineEnd The character to put at the end of each line, usually ','
223+
* or ';'
224+
* @param {string} typePrefix The prefix to put before the type name for
225+
* non-primitive types
226+
* @returns {undefined}
227+
*/
228+
function saveMsgFieldsAsTSD(msgInfo, fd, indent=0, lineEnd=',', typePrefix='') {
229+
const indentStr = ' '.repeat(indent);
194230
for (let i = 0; i < msgInfo.def.fields.length; i++) {
195231
const field = msgInfo.def.fields[i];
196-
const fieldType = fieldType2JSName(field);
197-
const tmpl = ` ${field.name}: ${fieldType}`;
232+
let fieldType = fieldType2JSName(field);
233+
let tp = field.type.isPrimitiveType ? '' : typePrefix;
234+
if (typePrefix === 'rclnodejs.') {
235+
fieldType = 'any';
236+
tp = '';
237+
}
238+
const tmpl = `${indentStr}${field.name}: ${tp}${fieldType}`;
198239
fs.writeSync(fd, tmpl);
199240
if (field.type.isArray) {
200241
fs.writeSync(fd, '[]');
201242
}
202-
if (i != msgInfo.def.fields.length - 1) {
203-
fs.writeSync(fd, ',');
204-
}
243+
fs.writeSync(fd, lineEnd);
205244
fs.writeSync(fd, '\n');
206245
}
246+
}
247+
248+
249+
function saveMsgInfoAsTSD(msgInfo, fd) {
250+
// write type = xxxx {
251+
const typeTemplate =
252+
` export type ${msgInfo.typeClass.name} = {\n`;
253+
254+
fs.writeSync(fd, typeTemplate);
255+
256+
// write field definitions
257+
saveMsgFieldsAsTSD(msgInfo, fd, 8);
207258

208259
// end of def
209260
fs.writeSync(fd, ' };\n');
@@ -223,7 +274,7 @@ function primitiveType2JSName(type) {
223274
switch (type) {
224275
case 'char':
225276
case 'byte':
226-
case 'uin8':
277+
case 'uint8':
227278
case 'int8':
228279
case 'int16':
229280
case 'uint16':
@@ -256,7 +307,7 @@ function fileName2Typeclass(filename) {
256307
const array = filename.split(regex).filter(Boolean);
257308

258309
if (!array || array.length != 3) {
259-
// todo: throw error
310+
// todo: throw error
260311
console.log('ERRORRROOROR', array);
261312
return;
262313
}

types/index.d.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
21
// eslint-disable-next-line spaced-comment
3-
/// <reference path="base.d.ts" />
2+
/// <reference path="base.d.ts" />
43

54
declare module 'rclnodejs' {
65

76
/**
87
* Create a node.
9-
*
8+
*
109
* @remarks
1110
* See {@link Node}
12-
*
11+
*
1312
* @param nodeName - The name used to register in ROS.
1413
* @param namespace - The namespace used in ROS, default is an empty string.
1514
* @param context - The context, default is Context.defaultContext().
@@ -19,15 +18,15 @@ declare module 'rclnodejs' {
1918

2019
/**
2120
* Init the module.
22-
*
21+
*
2322
* @param context - The context, default is Context.defaultContext().
2423
* @returns A Promise.
2524
*/
2625
function init(context?: Context): Promise<void>;
2726

2827
/**
2928
* Spin up the node event loop to check for incoming events.
30-
*
29+
*
3130
* @param node - The node to be spun.
3231
* @param timeout - ms to wait, block forever if negative, return immediately when 0, default is 10.
3332
*/
@@ -43,52 +42,53 @@ declare module 'rclnodejs' {
4342

4443
/**
4544
* Stop all activity, destroy all nodes and node components.
46-
*
45+
*
4746
* @param context - The context, default is Context.defaultContext()
4847
*/
4948
function shutdown(context?: Context): void;
5049

5150
/**
5251
* Test if the module is shutdown.
53-
*
52+
*
5453
* @returns True if the module is shut down, otherwise return false.
5554
*/
5655
function isShutdown(): boolean;
5756

5857
/**
5958
* Get the interface package, which is used by publisher/subscription or client/service.
60-
*
59+
*
6160
* @param name - The name of interface to be required.
6261
* @returns The object of the required package/interface.
6362
*/
63+
function require<T extends MessageTypeClassName>(name: T): MessageWrapperType<T>;
6464
function require(name: string): object;
6565

6666
/**
6767
* Generate JavaScript structs files from the IDL of
68-
* messages(.msg) and services(.srv).
68+
* messages(.msg) and services(.srv).
6969
* Search packages which locate under path $AMENT_PREFIX_PATH
70-
* and output JS files into the 'generated' folder.
70+
* and output JS files into the 'generated' folder.
7171
* Any existing files under the generated folder will
7272
* be overwritten.
73-
*
73+
*
7474
* @returns A Promise.
7575
*/
7676
function regenerateAll(): Promise<void>;
7777

7878
/**
79-
* Judge if the topic or service is hidden,
80-
*
79+
* Judge if the topic or service is hidden,
80+
*
8181
* @remarks
8282
* See {@link http://design.ros2.org/articles/topic_and_service_names.html#hidden-topic-or-service-names}
83-
*
83+
*
8484
* @param name - Name of topic or service.
8585
* @returns True if a given topic or service name is hidden, otherwise False.
8686
*/
8787
function isTopicOrServiceHidden(name: string): boolean;
8888

8989
/**
9090
* Expand a given topic name using given node name and namespace.
91-
*
91+
*
9292
* @param topicName - Topic name to be expanded.
9393
* @param nodeName - Name of the node that this topic is associated with.
9494
* @param nodeNamespace - Namespace that the topic is within.
@@ -99,7 +99,7 @@ declare module 'rclnodejs' {
9999

100100
/**
101101
* Create a plain JavaScript message object.
102-
*
102+
*
103103
* @param type - type identifier, acceptable formats could be 'std_msgs/std/String'
104104
* or {package: 'std_msgs', type: 'msg', name: 'String'}
105105
* @returns A Message object or undefined if type is not recognized.

0 commit comments

Comments
 (0)