Superstruct v0.10.0 Release Notes

Release Date: 2020-06-06 // about 2 years ago
  • 👍 The 0.10 version is a complete overhaul with the goal of making Superstruct much simpler and easier to understand, and with complete support for runtime type signatures TypeScript.

    This makes it much more powerful, however the core architecture has had to change to make it happen. It will still look very similar, but migrating between the versions will be more work than usual. There's no requirement to upgrade, although if you're using Superstruct in concert with TypeScript you will have a much better experience.

    BREAKING

    All types are created from factories. Previously depending on whether the type was a complex type or a scalar type they'd be defined different. Complex types used factories, whereas scalars used strings. Now all types are exposed as factories.

    For example, previously:

    import { struct } from 'superstruct'
    
    const User = struct.object({
      name: 'string',
      age: 'number',
    })
    

    Now becomes:

    import { object, string, number } from 'superstruct'
    
    const User = object({
      name: string(),
      age: number(),
    })
    

    Custom scalars are no longer pre-defined as strings. Previously, you would define all of your "custom" types in a single place in your codebase and then refer to them in structs later on with a string value. This worked, but added a layer of unnecessary indirection, and made it impossible to accomodate runtime type signatures.

    In the new version, custom types are defined extremely similarly to non-custom types. And this has the added benefit that you can easily trace the custom type definitions by just following import statements.

    Here's how it used to work:

    import { superstruct } from 'superstruct'
    import isEmail from 'is-email'
    
    const struct = superstruct({
      types: {
        email: isEmail,
      },
    })
    
    const Email = struct('email')
    

    And here's what it would look like now:

    import { struct } from 'superstruct'
    import isEmail from 'is-email'
    
    const Email = struct('email', isEmail)
    

    🚚 Validation logic has been moved to helper functions. Previously the assert and is helpers lived on the struct objects themselves. Now, these functions have been extracted into separate helpers. This was unfortunately necessary to work around limitations in TypeScript's asserts keyword.

    For example, before:

    User.assert(data)
    

    Now would be:

    import { assert } from 'superstruct'
    
    assert(data, User)
    

    Coercion is now separate from validation. Previously there was native logic for handling default values for structs when validating them. This has been abstracted into the ability to define any custom coercion logic for structs, and it has been separate from validation to make it very clear when data can change and when it cannot.

    For example, previously:

    const output = User.assert(input)
    

    Would now be:

    const input = coerce(input, User)
    

    The coerce step is the only time that data will be transformed at all by coercion logic, and the assert step no longer needs to return any values. This makes it easy to do things like:

    if (is(input, User)) {
      // ...
    }
    

    Validation context is now a dictionary of properties. Previously when performing complex validation logic that was dependent on other properties on the root object, you could use the second branch argument to the validation function. This argument has been changed to be a context dictionary with more information. The same branch argument can now be accessed as context.branch, along with the new information.

    Unknown properties of objects now have a 'never' type. Previously unknown properties would throw errors with type === null, however the newly introduced 'never' type is now used instead.

    0️⃣ Defaults are now defined with a separate coercion helper. Previously all structs took a second argument that defined the default value to use if an undefined value was present. This has been pulled out into a separate helper now to clearly distinguish coercion logic.

    For example, previously you'd do:

    const Article = struct.object(
      {
        title: 'string',
      },
      {
        title: 'Untitled',
      }
    )
    

    Whereas now you'd do:

    const Article = defaulted(
      object({
        title: string(),
      }),
      {
        title: 'Untitled',
      }
    )
    

    Optional arguments are now defined with a seperate factory. Similarly to defaults, there is a new optional factory for defined values that can also be undefined.

    Previously you'd do:

    const Flag = struct('string?')
    

    Now you'd do:

    const Flag = optional(string())
    

    Several structs have been renamed. This was necessary because structs are now exposed directly as variables, which runs afoul of reserved words. So the following renames have been applied:

    • interface -> type
    • enum -> enums
    • function -> func