Writing TypeScript Packages for Dynamics 365
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 -
- The ability to create, build and publish packages written in TypeScript for Dynamics 365
What’s wrong with form scripting?
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
- Create your package.json
- Configure XrmDefinitelyTyped. Use
crmVersionconfiguration parameters (ignore the rest).
- Generate the type definitions.
- Delete everything except the xrm.d.ts file.
- Run the TypeScript compiler using
- 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.
- Set the
typesproperty of your package.json to the path of the xrm.d.ts file.
- 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.
- Configure XrmDefinitelyTyped.
- Generate the type definitions.
- Delete the xrm.d.ts file.
- Add a dependency to your static definitions using
npm install @scope/package-name.
- Update your tsconfig.json file by adding
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
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.
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.
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).
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
importstatements 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
typesproperty include your package (it'll read the
typesproperty 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
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.
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.