Writing TypeScript Packages for Dynamics 365

“Life is a game board. Time is your opponent. If you procrastinate, you will lose the game.” — Napoleon Hill

This post is aimed at Dynamics 365 developers who have already made the move to TypeScript (and hopefully XrmDefinitelyTyped, too) and are interested in progressing from creating simple form scripts to authoring powerful, reusable packages that can interact with your forms.

If you’re not familiar with form scripting using XrmDefinitelyTyped then I recommend that you begin by reading their documentation. You should also have an understanding of package management with npm.

By the end of the post, you should have -

  • An understanding of the constraints that Dynamics 365 places on modern JavaScript/TypeScript
  • The ability to create, build and publish packages written in TypeScript for Dynamics 365

What’s wrong with form scripting?

Business rules aren’t powerful enough for anything other than simple forms, so JavaScript form scripts are a given on many projects. The Dynamics 365 community lags behind in the JavaScript stakes — especially in the midst of an increasingly rich ecosystem. Many of the tooling now available rarely features in the development processes of most Dynamics implementations. Form scripts are usually a cluster of functions that offer little insight as to the context in which they are used. They may also be both attached as event handlers on the form and called from other functions. Most cases are best described as spaghetti code.

TypeScript, XrmDefinitelyTyped and the ability to statically type a formContext or the (now deprecated) Xrm.Page object as a particular form makes things a lot easier. Static type-checking is a leap in the right direction but the paradigm of form scripting largely remains the same - code that isn't maintainable, easily understood or reusable. We recently started developing a rules engine using the processes explained in this post. The idea was to address the issues described and apply OO/SOLID principles to form scripting. Crucially, we wanted the engine to be written in TypeScript and compatible with our existing form scripts built with XrmDefinitelyTyped. I'll be using it as an example of what can be produced using this approach and, hopefully, illustrate the benefits.

Setting up your type definitions

There are (currently) two good sources of type definitions for the client API -

  • DefinitelyTyped is considered the de facto standard source of type definitions. It has type definitions for the client API that are not organisation specific.
  • XrmDefinitelyTyped is an open-source project by Delegate A/S which generates type definitions specific to your organisation.

The first thing to be aware of is that they are incompatible with each other. You cannot have the DefinitelyTyped type definitions in the same compilation context as the XrmDefinitelyTyped definitions. That’s a problem because both of these sources are independently useful, depending on the context in which they are used.

XrmDefinitelyTyped is more powerful than the static DefinitelyTyped definitions when it comes to form scripting — but what about generic web resources that are shared amongst many forms and across many organisations? These shouldn’t be dependent on form/organisation specific types. Thankfully, the files that XrmDefinitelyTyped generates are conveniently separated into both the dynamically generated contextual definitions and the static definitions of the client-side API. This allows you to write TypeScript that is only dependent on the definitions used by XrmDefinitelyTyped that aren’t tied to a given organisation — they can be packaged, shared and consumed everywhere.

Let’s look at how that actually works in practice. The idea is to share this code, so we’ll want to take advantage of a package manager (npm in this case).

Packaging the static types

  1. Create your package.json
  2. Configure XrmDefinitelyTyped. Use useDeprecated and crmVersion configuration parameters (ignore the rest).
  3. Generate the type definitions.
  4. Delete everything except the xrm.d.ts file.
  5. Run the TypeScript compiler using tsc xrm.d.ts.
  6. Compilation errors, if any, will be due to types that are dependent on the organisation specific types. Remove these types from the xrm.d.ts file.
  7. Set the types property of your package.json to the path of the xrm.d.ts file.
  8. Publish the package to a registry.

Your package.json should look something like the following (at a minimum)

You can now consume this package in a TypeScript project to create generic packages that interact with the client API and, importantly, they’ll be compatible with your XrmDefinitelyTyped form scripts.

The dynamic types

There is a very good argument for not distributing the dynamic types as an npm package. They should be kept up-to-date and projects shouldn’t be able to target an older version of the model — this will only lead to errors at runtime that should’ve been caught at compile-time. My preference is to have an XrmDefinitelyTyped model generated in the same project as my web resources.

  1. Configure XrmDefinitelyTyped.
  2. Generate the type definitions.
  3. Delete the xrm.d.ts file.
  4. Add a dependency to your static definitions using npm install @scope/package-name.
  5. Update your tsconfig.json file by adding @scope/package-name to the types array.

The final step allows the TypeScript compiler to compile the type definitions, as it has resolved the deleted xrm.d.ts file through the package. Your TypeScript project is now able to use the XrmDefinitelyTyped definitions and consume packages that target just the static types.

Setting up your package project

We’ve covered the static types package and the project that contains your form script, so let’s look at setting up a generic, reusable package. Below is the configuration for the xrm-form-engine

package.json

Note:

  • scripts - The build script runs TSLint, the TypeScript compiler and UglifyJS to create a minified version.
  • types - Points to the declaration file that the TypeScript compiler creates.
  • dependencies - There is a dependency on @capgemini/xrm-types, our static types package.
  • files - The package includes everything under the dist/ folder.
  • devDependencies - TSLint, TypeScript and Uglify are included as development dependencies as they are used for the build.

tsconfig.json

Note:

  • module - I don't believe we're able to use modules natively with Dynamics 365. There are other alternatives (e.g. writing the source as modules and using something like Browserify or Webpack to output a bundle) but I think setting this to 'none' and using namespacing is the simplest approach
  • declaration - This needs to bet to 'true' so that we can include the type definitions with our package.
  • outFile - This tells the compiler to concatenate all of our individual .ts files to a single .js file. The output is what gets uploaded to Dynamics 365 as a web resource.
  • types - This tells the compiler what packages to include the types for. In this instance, it's our static type definitions we extracted earlier
  • inlineSourceMap - Allows you to debug your TypeScript source by embedding the sourcemap in the outputted .js file. The alternative is to emit a separate .map file but this carries the overhead of an additional web resource.

tslint.json

It’s optional, but I use TSLint for code analysis when developing in TypeScript (be sure to install the TSLint extension for Visual Studio Code for IDE integration).

Note:

  • no-namespace - Disabled as we will be using namespaces instead of modules.
  • no-reference - Disabled as the reference directives are needed in the absence of import statements to inform the TypeScript compiler of dependencies.
  • variable-name - Overridden to allow a leading underscore. This is useful when creating properties that have a field of the same name behind them.

Developing your package project

If you’re using the same configuration as described above then all TypeScript in your src/ folder will be compiled with access to the static XrmDefinitelyTyped definitions. I recommend splitting your classes and interfaces up into their own files (TSLint will require this by default if you’re using it) and having namespaces that map onto your folder structure. Running the TypeScript compiler will output a .js file and a .d.ts file into the dist/ folder (and possibly a .min.js file if you’ve also configured UglifyJS). Once you’re ready, publish your package to a registry.

Consuming the package in your form scripts

First, install your package in your web resources project. At this stage, you can either -

  • Modify your tsconfig.json types property include your package (it'll read the types property of your package's package.json to resolve the correct file)
  • Add a triple-slash directive to the top of your form script e.g ///reference types="@capgemini/xrm-form-engine"/>

The second approach is better as it becomes explicit as to which form scripts are consuming this package. If you’ve done one of the above and your IDE/IntelliSense isn’t picking up the types then you might need to close and re-open the project — I’ve encountered this with Visual Studio Code occasionally.

Enjoy!


Update: I’ve spoken with Magnus Sørensen from Delegate A/S and proposed that XrmDefinitelyTyped supports the use of a separate package for the common xrm type definitions natively. Having a single package source would negate the need for the first step of this blog post and make it easier to share code that uses these types globally. You can track the issue on GitHub here.