Hello! Welcome back to our advanced Power Platform developer course.
In our last lesson, we mastered using Xrm.WebApi to perform client-side data operations. We saw how modern async/await syntax makes asynchronous code clean and readable, and we previewed how TypeScript could further improve our code by adding a layer of type safety and structure, especially when creating reusable helper functions.
Today, we're going to build on that preview. Your goal for this lesson is to develop and bundle client-side logic using TypeScript for improved type safety and maintainability. Given your strong background in C# and TypeScript, we won't dwell on the language basics. Instead, we'll focus on the practical, end-to-end process of setting up a professional development environment, writing type-safe code for Model-driven apps, and bundling it for deployment. This is a foundational skill for modern Power Platform development and a key topic for the PL-400 exam.
1. Why Use TypeScript for Model-Driven Apps?
While you're already familiar with TypeScript's advantages, it's worth framing them in the context of Model-driven apps. Plain JavaScript, especially in large, complex projects with multiple developers, can become difficult to maintain. TypeScript directly addresses these challenges.
A developer's guide to using TypeScript in Dynamics 365 ...
The article 'A developer's guide to using TypeScript in Dynamics 365' provides an excellent summary of why TypeScript is the preferred choice for professional client-side development in the Power Platform ecosystem. Let's start by quickly reviewing the key benefits.
Read the section 'Why choose TypeScript for Dynamics 365?'. Pay attention to how each point—type safety, enhanced tooling, maintainability, and ecosystem alignment—translates to tangible benefits in a Dynamics 365 project.
To summarize the key points you just read:
- Type Safety: Catching errors at compile time (e.g., mistyping a field name or passing the wrong data type) is far more efficient than discovering them at runtime.
- Enhanced Tooling: With type information, your IDE (like VS Code) can provide rich IntelliSense, autocompletion, and refactoring support for the
Xrmobject model. - Improved Maintainability: Interfaces, classes, and modules encourage a structured, object-oriented approach, making your code easier for you and your team to understand and extend.
- Ecosystem Alignment: TypeScript is the language of the Power Platform. Power Apps Component Framework (PCF) controls are built with it, and it aligns perfectly with modern DevOps practices using the Power Platform CLI—all topics that are relevant to your goals.
2. Setting Up a TypeScript Project
Let's get practical. To develop with TypeScript, you need a local development environment to write, compile (or more accurately, transpile), and bundle your code.
The process involves setting up a Node.js project, installing the necessary packages, and configuring the TypeScript compiler.
A developer's guide to using TypeScript in Dynamics 365 ...
The same article provides a clear, step-by-step guide to get your project configured. We will walk through this process together.
Follow the steps outlined in the 'Setting up TypeScript in Dynamics 365' section. This will guide you through: Prerequisites: Ensuring you have Node.js, npm, and VS Code. Installing TypeScript: npm install -g typescript (or npm install typescript --save-dev for a project-local installation, which is often preferred). Project Structure: Creating src and dist folders. Adding Xrm Type Definitions: Running npm install --save-dev @types/xrm. This is a crucial step that provides the type information for the entire client API object model. Configuring tsconfig.json: Creating the compiler configuration file.
Let's break down the tsconfig.json from the article. It's the heart of your TypeScript project:
{
"compilerOptions": {
"target": "es5", // Ensures compatibility with older browsers supported by Dynamics 365
"module": "commonjs", // A standard module system for Node.js environment; Webpack will handle this for the browser
"outDir": "./dist", // Where to output the compiled JavaScript files
"rootDir": "./src", // The root directory of your TypeScript source files
"strict": true, // Enables all strict type-checking options for higher code quality
"types": ["xrm"] // Explicitly includes the Xrm type definitions for IntelliSense
}
}
Once this is set up, you can start writing code in your src folder. For example, a simple function to toggle a field's visibility would look like this, with full type support:
// In src/forms/contactForm.ts
// The Xrm types give us access to namespaces like Xrm.Events
function onAccountTypeChange(executionContext: Xrm.Events.EventContext): void {
const formContext = executionContext.getFormContext();
// IntelliSense will suggest .getAttribute, .getControl, etc.
const accountType = formContext.getAttribute("custom_accounttype");
const discountField = formContext.getControl("custom_discountcode");
// The compiler knows .getValue() and .setVisible() exist and checks their parameters
if (accountType && discountField) {
discountField.setVisible(accountType.getValue() === "Premium");
}
}
3. Bundling with Webpack
You can't upload TypeScript files directly to Dataverse as web resources. They must be transpiled into JavaScript. Furthermore, as your project grows, you'll have many .ts files. Managing them as individual web resources is inefficient.
This is where a module bundler like Webpack comes in. Webpack takes all your TypeScript files and their dependencies (even third-party libraries) and bundles them into a single, optimized JavaScript file that you can upload to Dataverse.
TypeScript and Dynamics 365 / Model-Driven App client-side ...
The article 'TypeScript and Dynamics 365 / Model-Driven App client-side development – Part 2' provides an excellent, in-depth look at setting up Webpack for this exact purpose. It explains a critical concept that the first article touches on but doesn't detail: making your functions accessible to the form event handler.
Read the sections 'Adding and configuring webpack' and 'Library config'. Focus on understanding the purpose of ts-loader and, most importantly, the output.library section in the webpack.config.js file. This is the key to exposing your code to the global scope of the Model-driven App form.
The "Global Scope" Problem
As the article explains, a Model-driven App form needs to call your function by its name from the global scope (e.g., window.MyFunctions.onLoad). By default, Webpack bundles modules in a way that hides them from the global scope to prevent conflicts.
The output.library configuration in webpack.config.js solves this.
// A complete webpack.config.js example
const path = require('path');
module.exports = {
entry: './src/index.ts', // A single entry point for our library
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader', // Use the ts-loader to process TypeScript files
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'bundle.js', // The name of our final output file
path: path.resolve(__dirname, 'dist'),
// This is the crucial part!
library: {
name: 'MySolution', // The global variable name (e.g., window.MySolution)
type: 'var', // Expose it as a variable
},
},
};
To make a function available under this MySolution namespace, you must export it from your TypeScript files.
4. Professionalizing: Namespaces and Project Structure
For a large project, dumping all functions into a single global object isn't maintainable. A better approach is to create a nested namespace structure that mirrors your solution's logic.
TypeScript and Dynamics 365 / Model-Driven App client-side ...
The same Webpack article demonstrates a professional pattern for organizing your code into logical namespaces. This is a practice you'll want to adopt.
Read the section 'Adding “Namespaces” to the mix'. Observe how it refactors the project by: Creating entity-specific folders (e.g., src/forms/contact). Creating a main entry file (crm.ts in their example, which we'll call index.ts) that imports all the modules and assembles them into a structured object (www.CRM.Contact).
Let's adapt this powerful pattern. Imagine this file structure:
my-d365-project/
├── src/
│ ├── forms/
│ │ ├── account.ts
│ │ └── contact.ts
│ └── index.ts
├── dist/
├── node_modules/
├── package.json
├── tsconfig.json
└── webpack.config.js
src/forms/contact.ts:
// Note the 'export' keyword
export function onLoad(executionContext: Xrm.Events.EventContext): void {
console.log("Contact form loaded!");
// Your logic here...
}
export function onSave(executionContext: Xrm.Events.EventContext): void {
console.log("Contact form saved!");
}
src/index.ts (the main entry point):
// Import all functions from other modules
import * as contactForm from "./forms/contact";
import * as accountForm from "./forms/account"; // Assuming this file exists
// Assemble the final namespace object
export const Forms = {
Contact: contactForm,
Account: accountForm,
};
After running npx webpack, your dist/bundle.js file will contain a global MySolution object. You can then register your event handlers in the Model-driven App form editor like this:
- Function:
MySolution.Forms.Contact.onLoad - Function:
MySolution.Forms.Contact.onSave

Test your understanding!
You've created a new file, src/ribbon/quote.ts, which exports a function named recalculatePricing. Following the namespacing pattern above, what steps would you take in src/index.ts to make this function available, and what would be its fully qualified name to use in the Ribbon Workbench?
Show answer
-
In
src/index.ts:- First, you would import the new module:
import * as quoteRibbon from "./ribbon/quote"; - Then, you would add it to the exported namespace object, likely creating a new top-level namespace for ribbon commands:
(Note: You would need to adjust yourexport const Forms = { ... }; // Existing form logic export const Ribbon = { Quote: quoteRibbon, };index.tsto export a single object containingFormsandRibbonto match thewebpack.config.jslibrary.namesetup). A betterindex.tswould be:
And your webpack library would exposeimport * as contactForm from "./forms/contact"; import * as quoteRibbon from "./ribbon/quote"; export const Forms = { Contact: contactForm }; export const Ribbon = { Quote: quoteRibbon };MySolution.FormsandMySolution.Ribbon.
- First, you would import the new module:
-
Fully Qualified Name: The name to use in the Ribbon Workbench would be
MySolution.Ribbon.Quote.recalculatePricing.
Conclusion
You have now learned the complete, professional workflow for using TypeScript in Model-driven Apps. This approach moves you beyond simple, isolated JavaScript files to a scalable, maintainable, and modern development practice that is essential for enterprise-level projects.
Key Takeaways:
- TypeScript provides type safety and superior tooling, which are critical for building robust client-side logic in Dataverse.
- The development workflow involves setting up a Node.js project, installing TypeScript and the
@types/xrmdefinitions, and configuringtsconfig.json. - Webpack is used to transpile and bundle multiple TypeScript files into a single JavaScript web resource for deployment.
- The
output.librarysetting inwebpack.config.jsis essential for exposing your functions to the global scope of the Model-driven App. - Organizing your code with a namespacing pattern (e.g.,
Solution.Area.Entity.functionName) is a best practice for maintainability and scalability.
Up Next:
Now that you have a solid foundation in modern client-side scripting, our next lesson will focus on another key customization area: Customize the app command bar for tables and forms using the modern command designer. We will see how the JavaScript functions you now know how to build can be wired up to command bar buttons to trigger custom actions.