Skip to content

Embracing Modern Data Validation in EmberJS

September 23, 2023

16 min read

1.3K views

Introduction

In an EmberJS codebase I've been working with, Computed Properties and Mixins were previously used to handle Model validations, via ember-cp-validations. With Ember's shift away from Mixins and Computed Properties towards more standard JavaScript syntax and patterns, I wanted to detail how I wrote a new utility for validating Models, leveraging TypeScript, yup, and Tracked properties in order to leave behind these old patterns and provide stricter type safety.

The Why: So Long, Computed Properties

Computed Properties have long been a staple in Ember, serving as a mechanism for defining reactive properties based on dependencies. They rely on explicit annotations to list dependencies, making the code more verbose and less intuitive when compared to modern JavaScript patterns.

The previous paradigm worked as follows: buildValidations is called, provided with an object of properties on the Model to be validated, with their values being the result of the validator function - given a string representing the validator's name, and provided options, it returns a Computed Property. The returned result from buildValidations is an EmberObject Mixin, which can then be extended by the Model class to be validated:

Why was this paradigm in need of changing? With recent releases, Ember has been moving away from Computed Properties and several other Ember Classic patterns, including Mixins - this left us in an interesting spot, where Computed Properties and Mixins aren't technically deprecated, but it's safe to say they will be soon, and usage of either is discouraged. And that notwithstanding, they don't function quite right with Typescript - They work, but are difficult to use in otherwise-strictly-typed contexts and have issues when using Tracked primitives as dependant keys.

So, how do Tracked properties come into this? As a modern replacement for Computed Properties, they simplify state management and address some flaws of Computed Properties. By annotating a property with the @tracked decorator, Ember is informed that this particular field is likely to change over time and should trigger any dependent computations when it does. The result is a more straightforward, more explicit, less boilerplate-heavy approach to reactive state management.

A caveat is that Computed Properties also handled caching - native JavaScript getters recompute on each access, which usually isn't ideal. The @cached decorator from @glimmer/tracking can be used to cache the getter's value and avoid unnecessary recomputation. 

1

Moving away from Computed Properties also enables use of standard JavaScript syntax for property assignment and access, instead of EmberObject's .get()/.set() methods (mostly - the utility versions of these methods are still useful in some cases), and aligning classes more closely with native JavaScript patterns. Additionally, with Computed Properties, any property could theoretically be observed, potentially leading to unintended external dependencies or excessive re-computations. Tracked properties require explicit annotation, clearly delineating what properties are meant to be observed and reacted upon.

All that to say - there's many benefits from this switch ✨

Choosing a Library

The Landscape of Validation Libraries

Data validation libraries in the JavaScript and TypeScript ecosystem are abundant, catering to different paradigms, use-cases, and design philosophies. When it comes to type-safe validation for TypeScript specifically, several names come to the forefront—

, , and .

  • zod: Known for robustness and its rich feature set, zod excels in static type inference and allows for the use of Union and Intersection types.
  • io-ts: Originating from the functional programming world, io-ts offers an extremely type-safe validation mechanism. The library is based in functional programming paradigms.
  • yup: Originally created for use in JavaScript, yup was later refactored to TypeScript. It offers good ease-of-use and relatively strong TypeScript support, albeit with some limitations in type inference and missing schema functionality.

Some other libraries focusing more on input validation are:

  • validator.js: Great for simple, string-based validations, i.e. for form inputs, but it lacks the rich feature set and type safety required for more complex data structures.
  • Joi: Initially built for Hapi.js but usable standalone, Joi is another popular validation library. It's highly extensible but, like validator.js, less focused on type safety and more verbose compared to yup or zod.
  • class-validator: Works with TypeScript classes using decorators for validation logic. While tightly integrated with TypeScript, it couples validation logic directly with data models, which wasn't ideal for me.

There's also an entirely different class of libraries, like VeeValidate and react-hook-form which are framework-specific and concentrate on form validation. These can be useful for front-end validation for their respective UI library, but they don't offer the same flexibility as yup or zod when it comes to validating arbitrary data structures, nor do they usually offer type safety to the same degree.

Why 'Yup'?

So why yup over the other libraries mentioned? A few factors: First, it offers a level of simplicity that makes introducing it to other developers on the team much easier. Its syntax is intuitive, and it provides pretty comprehensive documentation. Second, although it was initially developed for JavaScript, its TypeScript support has greatly matured over the years. Its type inference may not be perfect (that honour's given to zod), but it's more than sufficient for most use-cases. Third, yup provides a high degree of flexibility, allowing for the validation of various data structures, not just form inputs or DOM elements.

yup stood out as a balanced choice for my use-case. It allows for easy schema definitions and validations, offers a fair level of type inference, and most importantly, doesn't require adopting a radically different programming paradigm. It is worth acknowledging its main shortcoming: schema type inference inaccuracies. Its type inference indicates that object properties are mandatory, when in actuality they're optional. In my opinion? The overall simplicity and developer experience offered outweighs this limitation. If you want truly strict & precise types, then zod or io-ts may be more compelling options.

Implementing the Utility

My objective was to offer type-safe validations for Models (or any other objects/classes, really), similar enough in structure to what ember-cp-validations provided in order to reduce refactoring work. Besides switching out validation schema and Mixins in Models, I wanted it to be (more or less) a "drop-in" replacement.

Class Blueprint

The YupValidations class takes in two type parameters, T extends FrameworkObject, representing the context where validation happens (often, this will be an EmberData Model), and S extends ObjectShape, representing a base yup schema of an empty object. On instantiation, the class takes the calling context and the schema shape to be used, initializing the schema with yup.object().shape():

Tracking Validation State

The @tracked decorator is used in the class to track any current validation errors. TrackedObject (from tracked-built-ins) is needed for the validation state, since Glimmer's @tracked decorator doesn't deep-track objects. This ensures any components accessing validations re-render when the validations change or are re-run.

Typed Validation Results

The class employs some utility types to type validation results based on the schema shape. FieldErrors<T> creates a mapping from each schema field to its validation messages, validity, and current/previous value. SchemaType<S, T> represents the shape of the given schema, and ValidationResult<S, T> represents the returned validation result.

Validating Data

The validate method triggers validations. It validates all context properties and updates the tracked validations property.

fieldErrors is a getter which simply presents any existing ValidationErrors in a more manageable shape. Likewise for #validationProperties.

Putting it All Together: Usage Example

Now that we've looked at the internals of the utility, here's an example of it in action - suppose you have a Route where you're dealing with an instance of MyModel and want to validate it before persisting any changes.

First, the model in question:

Now, let's call validate() on its Validator instance:

Wrapping Up

Building this out, I had a chance to dive much deeper into TypeScript utility types and generics than previously. I'm quite happy with the end result; a type-safe validation utility which can be used across our Ember application with confidence. Swapping out Computed Properties for Tracked properties isn't just a box-ticking exercise; it's a necessary step towards making the codebase more maintainable and future-proof.

My choice to use yup over other validation libraries came after weighing the trade-offs for this specific use-case. Ultimately, I think any of the mentioned libraries would be great fits, it just came down to my preference for yup's syntax.

I hope sharing this experience offers some valuable insights or ideas for your own projects, whether you're working with Ember or any other framework. If you have questions or want to discuss any part of this in more detail (cough cough, or if I made any mistakes),

!