delegateas / XrmDefinitelyTyped

Tool to generate TypeScript declaration files for Dynamics 365/CDS client-side coding.
http://delegateas.github.io/Delegate.XrmDefinitelyTyped/
MIT License
133 stars 53 forks source link

Be Able To Define Custom Interface Definition Creation #44

Closed daryllabar closed 6 years ago

daryllabar commented 6 years ago

Null Safe Code like this is really annoying

var att = Xrm.Page.getAttribute("name");
if(att){
    att.setValue("someValue");
}

Which is why I've seen lots of people creator nullsafe helper functions:

MyLibrary.setValue("name", "someValue");

These functions may log if the attribute isn't on the form, but it won't throw a null ref exception.

Is there a way to get the generated d.ts files, to create interface definitions for these custom library calls?

henrikhannemose commented 6 years ago

With XrmDefinitelyTyped we generate typings (i.e. a .d.ts file) for each form. There is thus no need to explicitly check if a field is on the form, since e.g. Xrm.Page.getAttribute("name") is only valid if you have an attribute called name on the given form:

XrmDefinitelyTyped form .d.ts

This should eliminate the need for explicitly checking if a field is on the form, since we already know it is.

In theory the field could still have been removed from the form since the last time you generated the typings with XrmDefinitelyTyped. We eliminate any possible errors by ensuring that we run XrmDefinitelyTyped and build our VS solution as part of our deployment pipeline.

Do you see any other use cases for creating custom interface definitions in XrmDefinitelyTyped?

daryllabar commented 6 years ago

True point, here is a couple counters.

  1. Many times I'll have very similar forms, and want to have a single JS file to handle all the forms. This could require multiple casts in order to get the form that has that particular field.
  2. 98% of the time, when I'm dealing with a lookup value, I don't want it in array form, I just want the selected item, in the array, so a typed helper function that returns back the entity reference rather than the array is nice.
  3. 98% of the time, when I'm dealing with control calls (set visible for example) I want to perform that action on every control for that attribute, so again a helper function that is typed to that would be helpful.
  4. As a contractor, I generally have very little control over what the client does with the forms when I'm gone. I'm not going to trust that they will regenerate the types and fix the JS files, so I would prefer to not fully trust the typings...
  5. I also provide overload helper function that accept a parameter to fire the on change event, so again, being able to have those helper functions typed, would be great...
magesoe commented 6 years ago
  1. XDT supports form intersections, which solves this problem. This is shown in our wiki

  2. We solve this issue by simply taking the first element of the array, but we agree that the behavior of always having entityreferences as an array is undesirable. We are currently discussing a better way of handling entityreferences as a whole.

  3. You can make a function that takes an XrmAttribute and the function you want to apply to each control.

    
    export function ForAllControls<T>(attr: Xrm.Attribute<T>, f: (c: Xrm.Control<Xrm.Attribute<T>>) => any) {
    attr.controls.forEach(x => f(x));
    }

ForAllControls(Page.getAttribute("accountnumber"), x => x.setDisabled(true));

This way you piggyback on the typings of XDT, such that you don't have to define the methods that are available on a control.

4. If you can't trust your typings, then you can't trust your typescript. We would advice you to setup a daily typings check of some kind, which could be done on a CI server. As @henrikhannemose mentioned, this is part of our deployment pipeline, but we also do a daily typings check.

5. You can make a function that takes an XrmAttribute as well as the onChange function.
```typescript
export function AddAndFire<T>(attr: Xrm.Attribute<T>, onChange: (c: Xrm.ExecutionContext<Xrm.Attribute<T>> | undefined) => any) {
  attr.addOnChange(onChange);
  attr.fireOnChange();
}

AddAndFire(Page.getAttribute("accountnumber"), fooChange);

As a general comment, we see the potential for allowing injection of interfaces like

declare namespace MyLibrary {
    function setValue(attrName: "name", value: string);
    function setValue(attrName: "phonenumber", value: number);
    function setValue(attrName: "somedate", value: Date);
}

But for different use cases than the ones you described.

daryllabar commented 6 years ago

I was not aware of 1. Thanks for pointing it out.

2, 3, and 5 are all workarounds that I'm not particularly excited about, but definitely aren't bad either.

Thanks for the through explanation and examples.

mktange commented 6 years ago

Got an idea over the weekend that I believe solves this, and it should also be quite simple to implement in XDT.

Basically, XDT just needs to generate an interface for each collection (attributes, controls, etc) for all form, that maps the key of the object to the wanted type. The custom library should be defined to utilize these interfaces, by using keyof and then applying the given key to get the correctly typed value.

In a form-script, the developer just has to declare the library to be of the wanted form-type (pretty much like you would do with Xrm.Page).

Here is a gist with an example:

https://gist.github.com/mktange/4878da0477dc1244a273af7359ec10b2

magesoe commented 6 years ago

Fixed in 53fc5821385b4749e64b971c4dde316e9c2a2dec