Mastering the implementation of type refinements for io-ts in processing input data

I have implemented io-ts for defining my typescript types. This allows me to perform runtime type validation and generate type declarations from a single source.

In this particular scenario, I aim to create an interface with a string member that needs to pass a regular expression validation, such as a version string. A valid input would resemble the following:

{version: '1.2.3'}

To achieve this, branded types seem to be the way forward. Here is the approach I have taken:

import { isRight } from 'fp-ts/Either';
import { brand, Branded, string, type, TypeOf } from 'io-ts';

interface VersionBrand {
  readonly Version: unique symbol; 
}

export const TypeVersion = brand(
  string, 
  (value: string): value is Branded<string, VersionBrand> =>
    /^\d+\.\d+\.\d+$/.test(value), 
  'Version' 
);

export const TypeMyStruct = type({
  version: TypeVersion,
});

export type Version = TypeOf<typeof TypeVersion>;
export type MyStruct = TypeOf<typeof TypeMyStruct>;

export function callFunction(data: MyStruct): boolean {
  const validation = TypeMyStruct.decode(data);
  return isRight(validation);
}

Although this setup successfully validates types within the callFunction method, attempting to call the function with a normal object results in a compilation error:

callFunction({ version: '1.2.3' });

The error message states that

Type 'string' is not assignable to type 'Branded<string, VersionBrand>'
.

While the error is logical due to Version being a specialization of string, I am looking for a way to enable callers to invoke the function with any string and then validate it against the regular expression at runtime without resorting to using any. Is there a way to derive an unbranded version from the branded version of a type in io-ts? And is utilizing a branded type the correct approach for this situation where additional validation is required on top of a primitive type?

Answer №1

It seems like you've posed a dual question, so I'll address both aspects.

Retrieving the base type from a branded type

One can inspect which `io-ts` codec is being branded by referencing the `type` field on an instance of `Brand`.

TypeVersion.type // StringType

Therefore, it's feasible to create a function that takes your struct type and generates a codec from its base. Alternatively, you could extract the type using the following approach:

import * as t from 'io-ts';
type BrandedBase<T> = T extends t.Branded<infer U, unknown> ? U : T;
type Input = BrandedBase<Version>; // string

Utilizing the branded type

Instead of the aforementioned method, consider defining a struct input type first and then refine it so that only specified parts of the codec are refined from the input. This can be done utilizing the newer `io-ts` API.

import { pipe } from 'fp-ts/function';
import * as D from 'io-ts/Decoder';
import * as t from 'io-ts';
import * as E from 'fp-ts/Either';

const MyInputStruct = D.struct({
  version: D.string,
});

interface VersionBrand {
  readonly Version: unique symbol;
}
const isVersionString = (value: string): value is t.Branded<string, VersionBrand> => /^\d+\.\d+\.\d+$/.test(value);
const VersionType = pipe(
  D.string,
  D.refine(isVersionString, 'Version'),
);

const MyStruct = pipe(
  MyInputStruct,
  D.parse(({ version }) => pipe(
    VersionType.decode(version),
    E.map(ver => ({
      version: ver,
    })),
  )),
);

In conclusion, adopting a branded type is recommended. It is advised to parse the `Version` string early in the process and utilize the branded type throughout the application. Additionally, incorporating parsing logic at the app's edge ensures stronger types within the core components.

Answer №2

One approach that has been successful for my specific scenario is defining an input type without any branding, and then refining it by taking the intersection of that type with a branding element. Here's an example:

export const TypeInput = type({
  version: string,
});

export const TypeRefined = intersection([
  TypeInput,
  type({
    version: TypeVersion,
  }),
]);

export type Version = TypeOf<typeof TypeVersion>;
export type RefinedType = TypeOf<typeof TypeRefined>;
export type InputType = TypeOf<typeof TypeInput>;

export function performValidation(data: InputType): boolean {
  // validate type
  const result = TypeRefined.decode(data);
  return isRight(result);
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I use a string variable in Angular 2 to create a dynamic template URL

@Component({ selector: 'bancaComponent', templateUrl: '{{str}}' }) export class BancaComponent implements OnInit { str: String; constructor(private http: Http) { } ngOnInit(): void { this.str = "./file.component.html"; } An ...

The random number generator in TypeScript not functioning as expected

I have a simple question that I can't seem to find the answer to because I struggle with math. I have a formula for generating a random number. numRandomed: any; constructor(public navCtrl: NavController, public navParams: NavParams) { } p ...

Attempting to transfer files to and from Firebase storage

Having trouble with my React Native app. I am trying to upload files, whether they are pictures or PDFs, but once uploaded, I can't seem to open them. However, "The files are getting uploaded to the storage." export const uploadToStorage = async (docu ...

Ionic causing delay in updating ngModel value in Angular 2

My ion-searchbar is set up like this: <ion-searchbar [(ngModel)]="searchQuery" (keyup.enter)="search();"></ion-searchbar> In the typescript file, the searchQuery variable is defined as follows: export class SearchPage { searchQuery: string ...

"Discover the step-by-step process of building a vue.js3 application with typescript, vue-router, and vuex without relying on

I have been assigned the task of developing a Vue3 application with TypeScript support using Vuex for state management and vue-router for basic routing. However, I am not allowed to use vue-cli for this project. Here is my current code: <head> & ...

The width of mat-table columns remains static even with the presence of an Input field

I'm currently working on an Angular component that serves the dual purpose of displaying data and receiving data. To achieve this, I created a mat-table with input form fields and used {{element.value}} for regular data display. Each column in the tab ...

Taking a segmented snapshot of a canvas using a flexible design scheme

I am working with a canvas that contains multiple div elements representing different sections of the canvas. However, when I capture these sections, they do not appear exactly as displayed on the screen. How can I track and accurately capture the div area ...

What is the best way to enforce constraints on three keys when validating an object using Joi?

Currently experimenting with Joi for object validation. Able to validate objects with constraints on two keys using any.when(). Now looking to implement validation with constraints on three keys, for example: var object = { dynamicPrize: false, ...

Effortlessly converting JSON data into TypeScript objects with the help of React-Query and Axios

My server is sending JSON data that looks like this: {"id" : 1, "text_data": "example data"} I am attempting to convert this JSON data into a TypeScript object as shown below: export interface IncomingData { id: number; t ...

The Enigmatic Essence of TypeScript

I recently conducted a test using the TypeScript code below. When I ran console.log(this.userList);, the output remained the same both times. Is there something incorrect in my code? import { Component } from '@angular/core'; @Component({ sel ...

Troubleshooting Problem in Angular 6: Difficulty in presenting data using *ngFor directive (data remains invisible)

I came across a dataset that resembles the following: https://i.sstatic.net/S0YyO.png Within my app.component.html, I have written this code snippet: <ul> <li *ngFor="let data of myData">{{data.id}}</li> </ul> However, when I ...

Identifying row expansion in ngx-datatable: detecting expand status on row click

Is there a way to determine if a line has already been expanded when using the toggle feature? When you click on a line, it expands and shows the details. Here is some code in HTML: <ngx-datatable #dataTable ... (select)='onRowSelect($eve ...

Version 1.9.3 of Redux Toolkit is encountering an error stating that the 'push' property is not found on the type 'WritableDraft<CartState>'

Currently delving into Redux Toolkit using TypeScript and facing a roadblock that seems deceptively simple. Unfortunately, my current knowledge isn't enough to unravel this puzzle and I'm in need of some guidance. The issue arises with an error ...

Tips for customizing the main select all checkbox in Material-UI React data grid

Utilizing a data grid with multiple selection in Material UI React, I have styled the headings with a dark background color and light text color. To maintain consistency, I also want to apply the same styling to the select all checkbox at the top. Althou ...

The inversify middleware is executed a single time

I utilize Inversify for object binding in the following manner: container.applyMiddleware(loggerMiddleware); let module = new ContainerModule((bind: interfaces.Bind) => { bind<Logger>(TYPES.Logger).toConstantValue(logger); bind<ILogger ...

Encountering Issues with TypeScript Strict in Visual Studio Code Problems Panel

I have discovered that I can optimize my TypeScript compilation process by utilizing the --strict flag, which enhances type checking and more. Typically, I compile my TypeScript code directly from Visual Studio Code with a specific task that displays the c ...

Mandatory classification eliminates understanding of function type

I'm currently trying to transform an optional argument from one type into a required one in my type definition. However, I am encountering some difficulties and can't seem to figure out what I'm doing wrong in the process. Would appreciate a ...

Encountering issues with accessing a variable before its initialization in the getServerSideProps function in

Currently, I am working on setting up Firebase and configuring the APIs and functions to retrieve necessary data in my firebase.tsx file. Afterwards, I import them into my pages/index.tsx file but I am encountering an issue where I cannot access exports af ...

Is your Angular5 service failing to transmit data?

I have two components in my code, A and B. Component A contains a form with data that I want to send to component B. However, it seems like component B is not receiving any data. Here is the code for Component A: import { MyService } from 'path/my ...

Customize time formatting in Angular to accommodate localized time formats

I am utilizing Angular's localization service version 8.3.28 to support English, Spanish (Mexico) with code es-MX, and Spanish (Spain) with code es-SP While using the date pipe: {{ foo.Date | date: 'shortDate' }} The dates are changing to ...