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 achieve the same functionality as C# LINQ's GroupBy in Typescript?

Currently, I am working with Angular using Typescript. My situation involves having an array of objects with multiple properties which have been grouped in the server-side code and a duplicate property has been set. The challenge arises when the user updat ...

React Router malfunctioning on production environment when integrated with an Express backend

My Single Page application is built using React for the frontend and Express for the backend. Within the application, there are two main components: and . The goal is to display the component when the "/"" URL is requested, and show the component for an ...

What steps can be taken to implement jQuery within an Angular 15 npm package?

In my development process, I often create my own npm packages using Angular and Typescript. One of the packages I am currently working on is a PDF viewer service, which includes a file named pdf-viewer.service.ts with the following code: import { Behavior ...

Unveiling typescript property guards for the unknown data type

Is there a way to type guard an unknown type in TypeScript? const foo = (obj: unknown) => { if (typeof obj === 'object' && obj) { if ('foo' in obj && typeof obj.foo === 'string') { r ...

Failure to update values in local storage using the React useLocalStorage hook

I've developed two unique custom hooks named useLocalStorage and useAuth. function getDefaultValue<T>(key: string, initialValue: T | null): T | null { const storedValue: string | null = localStorage.getItem(key); if (storedValue) { retur ...

Angular 7 and its scrolling div

Currently, I am working on implementing a straightforward drag and drop feature. When dragging an item, my goal is to scroll the containing div by a specified amount in either direction. To achieve this, I am utilizing Angular Material's CDK drag an ...

I am attempting to create a multi-line tooltip for the mat-icon without displaying " " in the tooltip

I attempted to create a multiline tooltip using the example below. However, the \n is showing up in the tooltip. I am looking to add a line break like we would with an HTML tooltip. Check out the code here. ...

What are the steps to creating an Observable class?

I am working with a class that includes the following properties: export class Model { public id: number; public name: string; } Is there a way to make this class observable, so that changes in its properties can be listened to? I'm hoping fo ...

When using TypeScript, a typed interface will not permit a value that is not within its specified string literal type

I have downsized my issue to a smaller scale. This class needs to set the default value of its "status" property. The type T extends the string literal type "PossibleStatus" which consists of 3 possible strings. Typescript is giving me trouble with this. ...

Ways to trigger the keyup function on a textbox for each dynamically generated form in Angular8

When dynamically generating a form, I bind the calculateBCT function to a textbox like this: <input matInput type="text" (keyup)="calculateBCT($event)" formControlName="avgBCT">, and display the result in another textbox ...

What could be causing this TypeScript class to not perform as anticipated?

My goal with this code snippet is to achieve the following: Retrieve a template using $.get(...), Attach an event listener to the input element within the template I am using webpack to transpile the code without encountering any issues. The actual cod ...

Tips on extracting value from a pending promise in a mongoose model when using model.findOne()

I am facing an issue: I am unable to resolve a promise when needed. The queries are executed correctly with this code snippet. I am using NestJs for this project and need it to return a user object. Here is what I have tried so far: private async findUserB ...

Why are the class variables in my Angular service not being stored properly in the injected class?

When I console.log ("My ID is:") in the constructor, it prints out the correct ID generated by the server. However, in getServerNotificationToken() function, this.userID is returned as 'undefined' to the server and also prints as such. I am puzz ...

Encountering a TS(2322) Error while Implementing Observables in Angular 12

Exploring the intricacies of Angular 12 framework, I find myself encountering a hurdle in my learning journey. The tutorial I am following utilizes the Observable class to query fixed data asynchronously. However, an unexpected ts(2322) error has surfaced ...

Troubles encountered while attempting to properly mock a module in Jest

I've been experimenting with mocking a module, specifically S3 from aws-sdk. The approach that seemed to work for me was as follows: jest.mock('aws-sdk', () => { return { S3: () => ({ putObject: jest.fn() }) }; }); ...

Universal Module Identifier

I'm trying to figure out how to add a namespace declaration to my JavaScript bundle. My typescript class is located in myclass.ts export class MyClass{ ... } I am using this class in other files as well export {MyClass} from "myclass" ... let a: M ...

When trying to access the "form" property of a form ElementRef, TypeScript throws an error

I've encountered an issue with accessing the validity of a form in my template: <form #heroForm="ngForm" (ngSubmit)="onSubmit()"> After adding it as a ViewChild in the controller: @ViewChild('heroForm') heroForm: ElementRef; Trying ...

The upcoming construction of 'pages/404' page will not permit the use of getInitialProps or getServerSideProps, however, these methods are not already implemented in my code

Despite my efforts to search for a solution, I have not found anyone facing the same issue as me. When I execute next build, an error occurs stating that I cannot use getInitalProps/getServerSideProps, even though these methods are not used in my 404.tsx f ...

the value of properrty becomes undefined upon loading

In my code, there exists an abstract class named DynamicGridClass, containing an optional property called showGlobalActions?: boolean?. This class serves as the blueprint for another component called MatDynamicGridComponent, which is a child component. Ins ...

How to delete an item from an object in TypeScript

Can you help with filtering an object in Angular or TypeScript to eliminate objects with empty values, such as removing objects where annualRent === null? Additionally, what method can we use to round a number like 2.833333333333335 to 2.83 and remove the ...