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

Allow generic type definitions & non-default exports #357

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

danielang
Copy link

feat(ts-interface-generator): handeling generic type definitions
fix(ts-interface-generator): handling for non-default export ManagedObjects

fix(ts-interface-generator): handling for non-default export ManagedObjects
@cla-assistant
Copy link

cla-assistant bot commented May 25, 2022

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ danielang
❌ danielang-ortec
You have signed the CLA already but the status is still pending? Let us recheck it.

@akudev
Copy link
Contributor

akudev commented May 30, 2022

@danielang Thanks a lot for your contribution!
Would you mind also providing a piece of documentation explaining what this allows and what typical use-cases would be?
I'm not yet sure about the best place for it, maybe in the Readme here or in the control development or control library sample. Better here, I guess. maybe in a new "Features" section.

@danielang
Copy link
Author

Hey @akudev,
sure 🙂
I added a section in the readme file of this package. I hope it goes in the right direction. I would be happy to receive some feedback.
The changes in this PR allows to use the ts-interface-generator also with named exports.
It also allows to make use of TypeScripts generic type parameters, optionally with type constrains.

Copy link
Contributor

@akudev akudev left a comment

Choose a reason for hiding this comment

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

Thanks, this looks good. Just some minor things grammar/typo nits. I'll have another look at the code later.

@@ -95,18 +95,75 @@ This is a problem for application code using controls developed in TypeScript as

This tool scans all TypeScript source files for <b>top-level definitions of classes</b> inheriting from `sap.ui.base.ManagedObject` (in most cases those might be sub-classes of `sap.ui.core.Control`, so they will be called "controls" for simplicity).

For any such control, the metadata is parsed, analyzed, and a new TypeScript file is constructed, which contains an interface declaration with the methods generated by UI5 at runtime.
For any such control, the metadata is parsed, analyzed, and a new TypeScript file is constructed, which contains an interface declaration with the methods generated by UI5 at runtime. This generated interface gets merged with the already existing code using TypeScripts [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) concept.
Copy link
Contributor

Choose a reason for hiding this comment

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

TypeScript's

Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence has been taken over into #476


### Handling `default` and named exports

In order that the Declaration Merging of TypeScript works, the modifiers of the generated interface has to match the parsed UI5 artifact.
Copy link
Contributor

Choose a reason for hiding this comment

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

Plural or singular? Decide for either "modifiers ... have to" or "modifier ... has to".

}
```

- **Default export:**
Copy link
Contributor

Choose a reason for hiding this comment

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

As regular UI5 controls are default exports of their module and custom control owners might strive for similar usage, I would mention this case first - before the named exports. (possibly also mention that standard UI5 controls are done like this)


- **Default export:**
```typescript
export default abstract class MyAbstractControl extends Control {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a particular reason for this control to be named "...Abstract..."? At first glance I would have expected both code samples to be as similar as possible apart from the difference which is supposed to be demonstrated.

}
```

becomes
Copy link
Contributor

Choose a reason for hiding this comment

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

"becomes" sounds like a transformation to me. What about being a bit more verbose and saying "for ... the following interface is generated" or so?
(same in the other samples)


### Generics

This tool also supports generic classes which extending `ManagedObject` class. In order to enable the Declaration Merging of TypeScript the generic type parameters needs to be incooperated into the generated interface declaration.
Copy link
Contributor

Choose a reason for hiding this comment

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

"which are extending" or I think better: "which extend" (no native speaker here, but we are not describing a current action, but rather a general state)


### Generics

This tool also supports generic classes which extending `ManagedObject` class. In order to enable the Declaration Merging of TypeScript the generic type parameters needs to be incooperated into the generated interface declaration.
Copy link
Contributor

Choose a reason for hiding this comment

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

Plural: "parameters need to be"
And:
I guess it's spelled "incorporated"

Copy link
Contributor

@akudev akudev left a comment

Choose a reason for hiding this comment

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

Code looks good to me as well, I'll try out one or two things with the changed code.

Object.prototype.hasOwnProperty.call(
existingImportsInSourceFile,
typeName
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity: how is this different from if (typeName in existingImportsInSourceFile)? Any inherited properties we should be aware of? Don't think so. Or just doing it like this in general to avoid inherited-property-problems in other places?
No need to change, just wondering.


let importSpecifier: ts.ImportSpecifier;

// TODO: Use a method to check for versions
Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed. :-) But outside the scope of this PR.

@akudev
Copy link
Contributor

akudev commented Jun 3, 2022

@danielang While the code is fine and the docu just has minor issues, I think there is a problem in how it works: when the generic class is used in its own API (and probably also when the class is used in APIs of other controls), then the generated methods in the interface do not have the generics part.

Example (shortened):

export default class SampleControl<T extends Toolbar> extends Control {
	static readonly metadata = {
		aggregations: {
			partner: { type: "SampleControl" }
		},
	};
}

causes the generation of

declare module "./SampleControl" {

    interface $SampleControlSettings extends $ControlSettings {
        partner?: SampleControl[] | SampleControl | AggregationBindingInfo | `{${string}}`;
    }

    export default interface SampleControl<T extends Toolbar> {
        // aggregation: partner
        getPartner(): SampleControl[];
        addPartner(partner: SampleControl): this;
        insertPartner(partner: SampleControl, index: number): this;
        removePartner(partner: number | string | SampleControl): this;
        removeAllPartner(): SampleControl[];
        indexOfPartner(partner: SampleControl): number;
        destroyPartner(): this;
    }
}

There's the class name "SampleControl" all over the place - without generics. Which makes TS complain: "Generic type 'SampleControl' requires 1 type argument(s).".

One could argue that the metadata actually it should be:

			partner: { type: "SampleControl<T extends Toolbar>" }

but this doesn't work either because

  1. the interface generator treats this string as a plain class name and tries to import the whole thing (fixable within the generator) and
  2. this string will still be there at runtime - and not be understood by UI5 (only fixable inside the babel transformer plugin, but that's the wrong place, as it shouldn't handle TypeScript input)

Right now I don't know how to deal with this. Of course writing types as strings in the metadata section is legacy not helping here, but it is what we have now and changing it would be a major development effort. We could also simply state that using generic classes in the control API definition is not supported.

What do you think?
And you, @codeworrior , @petermuessig ?

Copy link
Contributor

@akudev akudev left a comment

Choose a reason for hiding this comment

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

In #476 I have implemented, among other things, the "non-default exports" aspect of this change. So at least that part will be supported. As I wrote, there are some issues with the generics, so this remaining part of the PR looks tricky.

factory.createModifier(ts.SyntaxKind.ExportKeyword),
factory.createModifier(ts.SyntaxKind.DefaultKeyword),
],
classDeclaration.modifiers?.filter(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this does not cover cases like

class XYZ { ... }
...
export default XYZ;

(but I covered both in #476)

@@ -95,18 +95,75 @@ This is a problem for application code using controls developed in TypeScript as

This tool scans all TypeScript source files for <b>top-level definitions of classes</b> inheriting from `sap.ui.base.ManagedObject` (in most cases those might be sub-classes of `sap.ui.core.Control`, so they will be called "controls" for simplicity).

For any such control, the metadata is parsed, analyzed, and a new TypeScript file is constructed, which contains an interface declaration with the methods generated by UI5 at runtime.
For any such control, the metadata is parsed, analyzed, and a new TypeScript file is constructed, which contains an interface declaration with the methods generated by UI5 at runtime. This generated interface gets merged with the already existing code using TypeScripts [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) concept.
Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence has been taken over into #476

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.

3 participants