Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

better typescript typings #551

Merged
merged 17 commits into from
Feb 17, 2020
Merged

better typescript typings #551

merged 17 commits into from
Feb 17, 2020

Conversation

koonpeng
Copy link
Collaborator

  • generate type class strings for messages and services
  • narrow down TypeClass type from string to known values
  • generate typings for *Wrapper javascript classes
  • remove constants from generated types

Unfortunately it's not possible to provide strong typing for interfaces like require and createMessage because of typescript limitations (microsoft/TypeScript#34933). This PR at least allow intellisense for type class strings, e.g.

// 'std_msgs/msg/String' will show up in intellisense
node.createPublisher('std_msgs/msg/String', 'topic');

It also allows to use rclnodejs.require like so

// need explicit cast as conditional types are limited to 50 depth as of typescript 3.7.5
const ImageMarker = rclnodejs.require('visualization_msgs/msg/ImageMarker') as
  rclnodejs.visualization_msgs.msg.ImageMarkerWrapper;

// now able to reference constants
const circleConstant = ImageMarker.CIRCLE;

// can also create new instances
const newMarker = new ImageMarker();

This will work also,

const marker: rclnodejs.visualization_msgs.msg.ImageMarker = {
  // no need to define all constants anymore
  ... 
}

@minggangw minggangw self-requested a review January 21, 2020 04:56
@minggangw
Copy link
Member

Thanks for your PR and cleaning up the code, overall looks good and some small changes need to be done I think

  • The generated interfaces.d.ts failed through the eslint, see here, would you please take a look to resolve it?
  • Would you please merge these 5 commits into 1 and write a detailed commit log? I think what you wrote for this PR is enough. (the commit log is useful to track what we have done when publishing a new release)

I am going to fix the errors due to the wrong configuration of the CI (Windows & macOS), so we can get the green badges again, thanks!

@minggangw
Copy link
Member

CI failure has been fixed (b84455f), please rebase your branch.

@coveralls
Copy link

coveralls commented Jan 21, 2020

Coverage Status

Coverage decreased (-1.0%) to 90.007% when pulling 771e7b0 on koonpeng:develop into b84455f on RobotWebTools:develop.

@koonpeng
Copy link
Collaborator Author

The generated interfaces.d.ts failed through the eslint, see here, would you please take a look to resolve it?

should be fixed now

Would you please merge these 5 commits into 1 and write a detailed commit log? I think what you wrote for this PR is enough. (the commit log is useful to track what we have done when publishing a new release)

what we do here usually is to squash merge the PR with a suitable commit log. Should I open another PR with the commit squashed?

@minggangw
Copy link
Member

Would you please merge these 5 commits into 1 and write a detailed commit log? I think what you wrote for this PR is enough. (the commit log is useful to track what we have done when publishing a new release)

what we do here usually is to squash merge the PR with a suitable commit log. Should I open another PR with the commit squashed?

It's ok that I am going to squash these commits when merging, so please offer a proper commit log that I can paste it when squashing, thanks!

@koonpeng
Copy link
Collaborator Author

add generation of constants as const enum. They are special in that they will be inlined so the definitions need not exist in rclnodejs javascript code.

This would allow this to work

const newMarker: rclnodejs.visualization_msgs.msg.ImageMarker = {
  type: rclnodejs.visualization_msgs.constants.ImageMarker.CIRCLE,
  ...
}

generated js will have the constant inlined

const newMarker = {
  type: 0 /* CIRCLE */,
  ...
};

* 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 provides intellisense for typeclass strings and allow constants to be referenced by value.
@minggangw
Copy link
Member

@wayneparrott any comments on this PR, I am going to move forward to merge it if it looks good.

@wayneparrott
Copy link
Collaborator

Apologies been working on the ros2cli build-type enhancement. I’ll give this PR a look tomorrow if that is not too late.

@wayneparrott
Copy link
Collaborator

@koonpeng thx for creating this PR. I had a few mins today to merge it locally and test drive. I invite @minggangw to comment as well as your thoughts.

A couple of topics:

  1. Regarding how this PR generates IDL constants in the interface.d.ts file. Let me open the discussion with the question: should IDL constants be generated into their own namespace, e.g., visualization_msgs/constants/... rather than into visualization_msgs/msg/...?

My preference is NO. Put constants close to their message and message-wrapper, not separate namespace.

Here's where I'm coming from - my mental model for the interfaces.d.ts namespace structure. ROS has 3 types of communications: pub/sub, client/server and action. The namespace design of interfaces.d.ts follows this pattern <pkg>/<type>/message where <type> = [msg, srv, action]. This PR adds a new type to the list 'constants' which seems arbitrary. Here's how the namespace hierarchy created by this PR will appear:

  <pkg>/
      action/...        <-- TBD
      constants/...  <-- added by PR
      msg/...
      srv/...

Do we want to expand <pkg>/<type> such that type can include 'constants'. And the actual constants enums live in a separate namespace rather than the same namespace as the message that they apply.

If NO, an alternative is to append a suffix like 'Constants' to the generated enums and have them reside in the same namespace as the msg, e.g, instead of visualization_msgs/constants/ImageMarker generate visualization_msgs/msg/ImageMarkerConstants.

Thoughts?

  1. Now for a simpler topic. The PR introduces creation of the type-aliases: TypeClassStr, MessageClassStr and ServiceClassStr (see snippet below).
type MessageTypeClassStr = 
    'string' |
    'action_msgs/msg/GoalInfo' |
    ...
type ServiceTypeClassStr = 
    'action_msgs/srv/CancelGoal' |
    ...
 type TypeClassStr = MessageTypeClassStr | ServiceTypeClassStr;

Rather than the *Str naming suffix, in this case I prefer a *Name pattern resulting in: TypeClassName, MessageClassName, ServiceClassName. Since this is TypeScript, tooling will help developers to quickly determine the type is a string so no need for the 'Str'.

Thoughts?

@koonpeng
Copy link
Collaborator Author

Regarding how this PR generates IDL constants in the interface.d.ts file. Let me open the discussion with the question: should IDL constants be generated into their own namespace, e.g., visualization_msgs/constants/... rather than into visualization_msgs/msg/...?

I feel this is up for debate, I thought about several ways to do this but each has their pros and cons.

  1. Putting the constants in the same namespace as the message, and suffix it with 'Constants'. This puts the constants next to the type definition but I'm afraid of conflicting names. If a package has Foo and FooConstants messages for some reason, that will create a type Foo, const enum FooConstants, type FooConstants and const enum FooConstantsConstants. const enum FooConstants and type FooConstants will clash and there will be compilation errors.
    The same can happen to *Wrapper also but at least *Wrapper is an interface, if we change the main definition from type to interface, typescript will merge the interfaces, which will mess up the type definitions but at least there won't be compilation errors.

  2. Putting the constants in a separate namespace. This avoids name conflicts but as mentioned it messes up the organization of the messages, it also goes against the standard <pkg>/<type>/<name> ros message convention. Maybe an alternative would be to put constants outside the package namespace, like rclnodejs.constants.<pkg>.<type>.<name> but of course, it still has the downside that the constants are separated from the message definitions. And if we do this, should the *Wrappers move to their own namespace also?

  3. Try to follow rclnodejs as closely as possible, put the definitions in their own module under generated/<pkg>/<pkg>__<type>__<name>. But the generated classes are not supposed to be public in rclnodejs and are meant to be accessed with rclnodejs.require. Doing the equivalent in typescript will need some hacky casting

import { ImageMarkerWrapper } from 'rclnodejs/generated/visualization_msgs/visualization_msgs__msg__ImageMarker';
const ImageMarker = rclnodejs.require('visualization_msgs/msg/ImageMarker') as ImageMarkerWrapper`;
const circle = ImageMarker.CIRCLE;
// or just
const line = ImageMarkerWrapper.LINE;

The problem is that newer version of typescript sets a limit to the depth of conditional types, if microsoft ever decides to revert the change we would be able to avoid the casting and have something exactly the same as the js code.

const ImageMarker = rclnodejs.require('visualization_msgs/msg/ImageMarker');
const circle = ImageMarker.CIRCLE;

Now for a simpler topic. The PR introduces creation of the type-aliases: TypeClassStr, MessageClassStr and ServiceClassStr (see snippet below).

That's a good idea, it makes the naming more verbose.

@wayneparrott
Copy link
Collaborator

wayneparrott commented Jan 24, 2020

@koonpeng good feedback.
re: #1 and the potential for name collision, I gave it a quick consideration when drafted the prev post. I dismissed the concern, naively so. We should assume that a name collision will arise at some point because there is nothing to preclude a developer from defining X and XConstants idl msgs in the same package. I view your use of a 'constants' namespace as pragmatic and justifiable.

Assuming we proceed with this PR's use of a 'constants' namespace, I have 3 additional thoughts.

  1. For consistency with <pkg>/msg and <pkg>/srv rather than use the name 'constants' for the namespace containing msg idl constants, we use the singular form 'constant' instead.
  <pkg>/constant/...
  <pkg>/msg/...
  <pkg>/srv/... 
  1. Thoughts on introducing a 'wrapper' namespace in which reside a package's wrapper interfaces? This may be taking things just too far but I did want to bring it up for discussion.

  2. Lastly, I wanted to inquire if IDL default values are handled presently? I'm not aware of the current state of support for default values defined in an interface. Assuming default values are supported, how might they be represented in a ts declaration file? Is it even possible and does this have any influence on how we generate type-decls?

@wayneparrott
Copy link
Collaborator

@koonpeng this might help in the discussion. Just for fun I took a look at /opt/ros/eloquent/share/visualization_msgs/msg/ImageMarker.idl on my system. Notice the generation of the ImageMarker_Constants just before the ImageMarker struct.

// generated from rosidl_adapter/resource/msg.idl.em
// with input from visualization_msgs/msg/ImageMarker.msg
// generated code does not contain a copyright notice

#include "builtin_interfaces/msg/Duration.idl"
#include "geometry_msgs/msg/Point.idl"
#include "std_msgs/msg/ColorRGBA.idl"
#include "std_msgs/msg/Header.idl"

module visualization_msgs {
  module msg {
    module ImageMarker_Constants {
      const uint8 CIRCLE = 0;
      const uint8 LINE_STRIP = 1;
      const uint8 LINE_LIST = 2;
      const uint8 POLYGON = 3;
      const uint8 POINTS = 4;
      const uint8 ADD = 0;
      const uint8 REMOVE = 1;
    };
    struct ImageMarker {
      std_msgs::msg::Header header;

      @verbatim (language="comment", text=
        " Namespace which is used with the id to form a unique id.")
      string ns;

      @verbatim (language="comment", text=
        " Unique id within the namespace.")
      int32 id;

      @verbatim (language="comment", text=
        " One of the above types, e.g. CIRCLE, LINE_STRIP, etc.")
      int32 type;

@koonpeng
Copy link
Collaborator Author

I just saw this here http://design.ros2.org/articles/interface_definition.html

Each file contains a single message or service. Message files use the extension .msg, service files use the extension .srv.

Both file names must use an upper camel case name and only consist of alphanumeric characters.

A quick test indeed shows that messages cannot have underscore, so a suffix of _Constants should be safe for any conflicts.

If there is no objections, I will go ahead and change the constants to use _Constants suffix.

For the *Wrapper messages, I am thinking about moving them to their own modules to reflect the path of the js codes. The *Wrapper interfaces is different in that they represent actual js classes, the constants and the main definitions on the other hand does not map to any js objects.

The advantage is that this allows the *Wrapper interfaces to be imported (although they are not supposed to be public in rclnodejs), and it would probably be cleaner to properly define them in the correct location.

@minggangw
Copy link
Member

I am on the Chinese New Year holidays and just come back home 😃 By looking through the comments quickly, I think it's feasible to add the suffix for the const values. So I think we can move forward, @wayneparrott please help to merge it if both of you agree with the implementation when PR is ready.

@wayneparrott
Copy link
Collaborator

@koonpeng Thx for the PR update. I locally merged this PR and generated messages and interface.d.ts.

  1. While I think I understand the *Wrappers I feel it will be useful if you can you provide a small example that demonstrates how one would use the msg, constants and wrapper? Alternatively, I can provide one and you can critique how new devs might interpret the use of wrappers.

  2. Looking at the wrapper implementations, they follow the pattern shown below of a module, namespace and class with import from the rclnodejs module. Are there design time performance concerns that we should consider? I'm thinking of coding tools that will load this declaration file. Specifically will the wildcard import from rclnodejs repeated for every msg-wrapper be expensive?

declare module 'rclnodejs/generated/std_msgs/std_msgs__msg__String' {
  import * as rclnodejs from 'rclnodejs';
  class StringWrapper {
    data: string;
    constructor(other?: rclnodejs.std_msgs.msg.String);
  }
  namespace StringWrapper { }
  export = StringWrapper;
}

@koonpeng
Copy link
Collaborator Author

The wrapper are in a weird situation now, originally I wrote the wrappers to provide type information in rclnodejs.require. I was trying to do it using conditional types.

type WrapperFromName<T> = T extends 'visualization_msgs/msg/ImageMarker' ? ImageMarkerWrapper :
  T extends 'visualization_msgs/msg/Marker' ? MarkerWrapper : never;
  ...

require<T extends TypeClassName>(typeClassName: T): WrapperFromName<T>;

But I got into a roadblock as typescript limits the depth of conditional types to 50. This puts the wrapper in this weird situation

  1. To get type information, you need to cast the return of rclnodejs.require.
  2. To perform the cast, you need to import the wrapper class.
  3. If you import the wrapper class, there would be no need for rclnodejs.require as you could just use the import directly.

This is further complicated by the fact that the recommended way to access the constants in js is through rclnodejs.require, but in ts using rclnodejs.require without the above mentioned cast will not give you type information, and also that there is a _Constants const enum so it's better to not use rclnodejs.require at all.

Maybe there is another way to provide type information for rclnodejs.require that I'm not aware of.


A simple compile test does show less performance with the wrappers, although not by a lot.

with import rclnodejs

[eloquent]teokp@teokp-desktop:~/source/test$ time npx tsc

real    0m1.653s
user    0m3.238s
sys     0m0.063s

without import rclnodejs

[eloquent]teokp@teokp-desktop:~/source/test$ time npx tsc

real    0m1.540s
user    0m3.029s
sys     0m0.068s

@minggangw
Copy link
Member

I am trying to follow the current problem we meet here and have some ideas to share with you if it's useful. Now we leverage require to get the needed information from .js files generated by rosidl_gen in rostsd_gen, like:

function createMessage(type) {
let typeClass = loader.loadInterface(type);
return typeClass ?
new typeClass() :
undefined;
}

So the overhead is to do a traversal of all messages, read them from disk and write the interfaces.d.ts to disk finally which may lead to the concern of performance. I am thinking that if we can combine (maybe integrate is more accurate ) the generation of interfaces.d.ts into that of non-typescript files. Probably, we could pass the spec

const generatedCode = removeExtraSpaceLines(dots.message({
messageInfo: messageInfo,
spec: spec,
json: JSON.stringify(spec, null, ' '),
}));

to

function savePkgInfoAsTSD(pkgInfos, fd) {

If it's feasible we have 2 things to ensure:

  1. If the spec object generated by the parser can offer all the necessary information to generate the typescript interface?
  2. Can it resolve the "typescript limits the depth of conditional types to 50" by doing so?

Correct me if anything is wrong, thanks!

@koonpeng
Copy link
Collaborator Author

koonpeng commented Feb 3, 2020

I believe @wayneparrott is talking about parsing the generated typescript definitions, the typescript maximum depth is also an issue with typescript refusing to compile the generated definitions. Combining the js and ts generation may improve performance but the problem is in the generated definitions and not the processing of generating them.

For the wrappers I am just not sure how to provide any usage examples as their usage are questionable. Ideally their usage should be hidden from the user like the js code, the user should use rclnodejs.require with the typeclass name instead of require('rclnodejs/generated/...') directly. But because of the typescript limitations this is not possible. Until we have a solution to provide type information for rclnodejs.require the wrapper should only be used when absolutely necessary, for example when you need to create a message with default values and you would like to have type information for it.

@minggangw
Copy link
Member

but the problem is in the generated definitions and not the processing of generating them.

Thanks for the clarification, it's clear now. I spend some time to learn the concept of Conditional Types in typescript and have a deep understanding now. I am afraid the developers may have a little bit confused about the "Wrapper" due to the explicit cast when using require.

@wayneparrott
Copy link
Collaborator

wayneparrott commented Feb 3, 2020

@koonpeng
It's unfortunate TS is not fully capable (yet) to support conditional types as we would use them. That would be a good enhancement.

For the wrappers I am just not sure how to provide any usage examples as their usage are questionable

I've wondered about this as well and have the following questions:

  1. should we generate ts for wrappers at this time or wait until deep conditional types or an alternative mechanism is available ?
  2. if we do, should wrappers continue to be included in interface.d.ts or in their own d.ts file since we suspect they may be referenced infrequently?

Later today I plan to try a few experiments using wrappers to improve my understanding of the developer experience working with the msgs and wrappers.

@koonpeng
Copy link
Collaborator Author

Sorry for the late response as I was busy with other stuffs, I think the wrappers could still be useful for some edge cases. It shouldn't hurt performance too much if we move them to their own dirs.

@mattrichard
Copy link
Collaborator

mattrichard commented Feb 10, 2020

As an avid TypeScript user, I'd like to chime in here.

There shouldn't be any need for complex/nested conditional types here. To get the type associated with a message you can use a type map in combination with the keyof operator + indexing, and finally add a basic conditional type to support string or object.

Example (all type names are purely for example only):

// Define all messages mapped to their respective type
type Messages = {
  'action_msgs/msg/GoalInfo': action_msgs.msg.GoalInfo;
  'action_msgs/msg/GoalStatus': action_msgs.msg.GoalStatus;
  // ...
}

// Define all services mapped to their respective type
type Services = {
  'action_msgs/srv/CancelGoal_Request': action_msgs.srv.CancelGoal_Request;
  'action_msgs/srv/CancelGoal_Response': action_msgs.srv.CancelGoal_Response;
  // ...
}

// These will contain the exact same values as before
type MessageTypeClassName = keyof Messages;
type ServiceTypeClassName = keyof Services
type Message = Messages[MessageTypeClassName] | Services[ServiceTypeClassName];
type TypeClassName = MessageTypeClassName | ServiceTypeClassName;

// Split original TypeClass to separate messages from services
type MessageTypeClass =
  (() => any) |
  MessageTypeClassName |
  { package: string; type: string; name: string };

type ServiceTypeClass =
  (() => any) |
  ServiceTypeClassName |
  { package: string; type: string; name: string };

type TypeClass = MessageTypeClass | ServiceTypeClass;

// Now define our conditional types to support message name or object
type MessageType<T extends MessageTypeClass> = T extends MessageTypeClassName ? Messages[T] : Messages[MessageTypeClassName];
type ServiceType<T extends ServiceTypeClass> = T extends ServiceTypeClassName ? Services[T] : Services[ServiceTypeClassName];
type MessageOrServiceType<T extends TypeClass> = T extends TypeClassName ? (Messages & Services)[T] : Message;

We can then leverage these types in require, createMessageObject, createSubscription, createPublisher, etc by using generics.

// Example usage: rclnodejs.require('sensor_msgs/msg/CompressedImage')
//
// Expands to:
// rclnodejs.require<"sensor_msgs/msg/CompressedImage">(
//   name: "sensor_msgs/msg/CompressedImage"): rclnodejs.sensor_msgs.msg.CompressedImage
function require<T extends TypeClass>(name: T): MessageOrServiceType<T>;

// Example usage (with correct callback message type):
// 
// this.node.createSubscription('sensor_msgs/msg/CompressedImage', ...)
function createSubscription<T extends MessageTypeClass>(typeClass: T, topic: string,
   options: Options, callback: SubscriptionCallback<MessageType<T>>): Subscription;

function createPublisher<T extends MessageTypeClass>(typeClass: T, topic: string, options?: Options): Publisher;

function createMessageObject<T extends TypeClass>(type: T): MessageOrServiceType<T>

where SubscriptionCallback is defined as follows

type SubscriptionCallback<T extends Messages[MessageTypeClassName]> = (
  (message: T) => void
);

This solution ensures the proper types without the need for any casting or deep conditional operators. The example above can be improved much further (for example you can get rid of the conditional types completely by switching to method overloads, which will simplify things), but this should at least give the general idea.

@mattrichard
Copy link
Collaborator

In response to my last comment, I'd recommend using function overloads over conditional types to avoid unnecessary complexity. The example above is simply to show that we don't need deeply nested conditional types to achieve strong typing for require and createMessageObject.

@koonpeng
Copy link
Collaborator Author

@mattrichard Thanks for the suggestion! I felt that I am missing something but im not proficient enough in typescript to find the solution. If my understanding for the function overloads is right, it will go like this?

function require(name: 'foo'): Foo
function require(name: 'bar'): Bar
...
function require(name: string): object

Do we have to declare each overload for require, createPublishers etc that uses the typeclass names separately? If so I see a possible downside is that we would have to generate node.d.ts, index.d.ts etc instead of only having to do it in interfaces.d.ts and referencing the types at other places.

@mattrichard
Copy link
Collaborator

The amount of overloads should be fixed so we don't have to worry about generating any additional declaration files. Taking require as an example, it only uses the conditional type as a return type so we can define it twice, one for each return type.

function require<T extends TypeClassName>(name: T): (Messages & Services)[T];
function require(name: string | (() => any) | { package: string; type: string; name: string }): Message;

We could also split up the union type into additional overloads if we wanted, but the only benefit of doing this is that it would allow documenting each overload separately.

function require<T extends TypeClassName>(name: T): (Messages & Services)[T];
function require(name: { package: string; type: string; name: string }): Message;
function require(name: () => any): Message;
function require(name: string): Message;

Either way works. If you then follow this same pattern with createPublisher, createMessageObject, etc. you'll be able to remove the conditional types from the example in my comment above along with the XTypeClass types, greatly simplifying the typings while preserving the strong typing.

There's a lot more enhancements that can be made, but this is a good start. Ultimately, it's up to the maintainers to decide how far we take this.

@wayneparrott
Copy link
Collaborator

@mattrichard Thx for sharing your insights on how to more fully leverage typescript in this project. We want the typescript dimension of this project to be as proper, complete and attractive to incoming ts developers as we can make it.

I took the 1st baby step by adding the initial d.ts generation. @koonpeng has been pushing forward to fix my goofed code-gen of msg constants and intro a more strongly typed api where msgs/srvs are involved. Your proposal is consistent with how we would like to see typescript declaration support expand for the project.

There's a lot more enhancements that can be made, but this is a good start. Ultimately, it's up to the maintainers to decide how far we take this.

While the extent of project enhancements is up to our lead @minggangw it would be interesting to hear additional insights and enhancement ideas.. Please consider opening a new topic and sharing your thoughts.

@koonpeng
Copy link
Collaborator Author

I do see several places now that can be improved, for example in createPublisher (and pretty much every where else that uses Message) we can have a generic type.

// generic publisher
class Publisher<T> extends Entity {
   publish(message: T): void;
   ...
}

// typed overload
createPublisher<T extends MessageTypeClassName>(typeClass: T, topic: string, options?: Options): Publisher<MessageType<T>>;
// general overload
createPublisher(typeClass: string | (() => any) | { package: string; type: string; name: string }, topic: string, options?: Options): Publisher<object>;

But I would keep this PR to only focus on the context of constants and rclnodejs.require, other PRs could be made to overhaul the typescript support in the future.


On a side node, the CI is failing due to missing dependencies, I'm not sure if it is caused by the changes on this PR.

@mattrichard
Copy link
Collaborator

@koonpeng you're correct about making the Publisher class generic, but I agree with keeping the PR focused. I'll open an issue as @wayneparrott suggested about improving TypeScript support even further.

BTW, it looks like the generated types caused a lint error.

> eslint --max-warnings=0 index.js types/*.d.ts scripts lib example rosidl_gen rosidl_parser test benchmark/rclnodejs && node ./scripts/cpplint.js

C:\proj\rclnodejs\types\interfaces.d.ts
  3449:1  error  Line 3449 exceeds the maximum line length of 120  max-len
  3450:1  error  Line 3450 exceeds the maximum line length of 120  max-len

That might be difficult to address since we have no control over package name lengths.

@minggangw
Copy link
Member

I think it's reasonable for us to improve the functionality of typescript step by step instead of doing it in a signal commit, considering that we have discussed a lot and have a clear direction. So @wayneparrott please help to merge this PR when you think it's ready, thanks!

3450:1 error Line 3450 exceeds the maximum line length of 120 max-len

I think we should exclude the generated files from lint, as we don't need to keep it readable and add /* eslint-disable */ can get rid of it.

@wayneparrott
Copy link
Collaborator

@koonpeng I'll test out your latest update later today. Appreciate all the work you've done to improve ts support. fyi: it's a little intimidating (but learning process) participating in the ts development with you and @mattrichard. I'll do my best to perform at a proficient level.

@wayneparrott
Copy link
Collaborator

I loaded your PR and generated msgs and types. Nice work! I like the constants and smart type support for require(). One thing you can help me with is that I don’t understand the role and use of the constant-like properties defined on wrapper types. As an example see the Log_Wrapper below.

export const enum Log_Constants {
        DEBUG = 10,
        INFO = 20,
        WARN = 30,
        ERROR = 40,
        FATAL = 50,
      }
export type Log = {
        stamp: builtin_interfaces.msg.Time,
        level: number,
        name: string,
        msg: string,
        file: string,
        function: string,
        line: number,
      };
export type Log_Wrapper = {
        DEBUG: number,
        INFO: number,
        WARN: number,
        ERROR: number,
        FATAL: number,
        new(other?: Log): Log,
      }

Here’s how I am using the new constant definitions:

const log = new (rclnodejs.require('rcl_interfaces/msg/Log'));
log.level = rclnodejs.rcl_interfaces.msg.LogConstants.DEBUG;

Is this the correct approach?
If yes what is the intent of the constant-like properties of the Log_Wrapper? Please share a snippet of their use if possible.

Other than this question, I have no issues with the PR.

@minggangw minggangw removed their request for review February 15, 2020 09:46
@koonpeng
Copy link
Collaborator Author

An example usage would be

const Log = rclnodejs.require('rcl_interfaces/msg/Log');
const log = new Log();
log.level = Log.DEBUG;

It's actually the same code as js.

There are 3 types generated now Log_Constants, Log and Log_Wrapper.

Log_Constants is leftovers from the previous implementation, its not used in the example above. Should this be removed to keep things more concise?

Log_Wrapper is the constructor function of LogWrapper from js and Log is the instance type. This is because rclnodejs.require returns a constructor function, which you assign to the variable Log, and a new instance is created with new Log(). Normally, if we were to translate LogWrapper to typescript to would be something like

class LogWrapper {
  static readonly DEBUG: number = 1;
  static readonly INFO: number = 2;
  level: number;
  constructor(other?: LogWrapper);
}

But we can't have rclnodejs.require(): LogWrapper as that would mean it returns an instance of it, and not something which you can new. So Log_Wrapper describes something which you can new on, and doing so would return an instance of Log.

I noticed I should change the properties of Log_Wrapper to readonly and maybe change the name to Log_WrapperType to make things more clear.

@mattrichard
Copy link
Collaborator

mattrichard commented Feb 16, 2020

Out of curiosity is there any particular reason for using type for everything? Since messages are new-able shouldn't we declare them as classes? Seems like the Log types can combined.

declare module 'rclnodejs' {
  namespace rcl_interfaces {
    namespace msg {
      export class Log {
        static readonly DEBUG: 10;
        static readonly INFO: 20;
        static readonly WARN: 30;
        static readonly ERROR: 40;
        static readonly FATAL: 50;
        constructor(other?: Log);
        stamp: builtin_interfaces.msg.Time;
        level: number;
        name: string;
        msg: string;
        file: string;
        function: string;
        line: number;
      }
    }
  }
}

Then if I'm not mistaken you can use typeof to get the constructor function type: rclnodejs.require(): typeof rcl_interfaces.msg.Log

@koonpeng
Copy link
Collaborator Author

just a very small thing I notice is that putting things as a class may create invalid code.

import * as rclnodejs from 'rclnodejs';

const test = new rclnodejs.rcl_interfaces.msg.Log(); // typescript compiles fine but generated js is invalid
// const test2 = new rclnodejs.rcl_interfaces.msg.Log_Wrapper(); // typescript gives error

the code is generated in js as

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const rclnodejs = require("rclnodejs");
const test = new rclnodejs.rcl_interfaces.msg.Log();

rclnodejs.rcl_interfaces.msg.Log doesn't actually exists in js, so trying to run that code will give an error.

@wayneparrott
Copy link
Collaborator

Log_Constants is leftovers from the previous implementation, its not used in the example above. Should this be removed to keep things more concise?

My vote is yes, discontinue generation of the constants.
@mattrichard @minggangw thoughts?

@mattrichard
Copy link
Collaborator

rclnodejs.rcl_interfaces.msg.Log doesn't actually exists in js, so trying to run that code will give an error.

Interesting. After this PR goes in I'll take a look at things more closely. I've been mainly replying based on the comments and hardly looking at the code.

My vote is yes, discontinue generation of the constants.

That's fine with me.

@minggangw
Copy link
Member

I agree, @wayneparrott please help to merge and past the commit log of 985c1f7 when squashing the commits.

Copy link
Member

@minggangw minggangw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we have unanimously approved this PR, let me merge it now. I'd like to thank @koonpeng for the contribution and patience, we learn a lot through the discussion 👍

@minggangw minggangw merged commit a04c6d2 into RobotWebTools:develop Feb 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants