DEV Community

Discussion on: Using fp-ts and io-ts: types and implementation

Collapse
 
ruizb profile image
Benoit Ruiz

Hello, thank you for your feedback!

If you want to avoid using a type assertion there, you can create a predicate function:

const isNonEmptyString60 = (input: unknown): input is NonEmptyString60 =>
  t.string.is(input) && isNonEmptyString(input) && input.length <= 60
Enter fullscreen mode Exit fullscreen mode

And use it both for the type guard and validate functions of your codec:

const NonEmptyString60 = new t.Type<NonEmptyString60, string, unknown>(
  'string',
  isNonEmptyString60,
  (input, context) => (isNonEmptyString60(input) ? t.success(input) : t.failure(input, context)),
  String
)
Enter fullscreen mode Exit fullscreen mode

Since everything newtypes-ts does is also possible with branded types in io-ts, I'd only use io-ts. The only downside I see is that newtypes-ts exposes convenient types (such as Integer, Char or NonEmptyString) that must be reimplemented using t.Branded.

(there's actually a NonEmptyString codec and a fromNewtype function in the io-ts-types package, but I couldn't manage to make this function work. I believe it uses the newer io-ts APIs, which are still marked as experimental as of today. Feel free to give it a try though :) )


On a side note, I'd use t.brand instead of defining a new t.Type, as it requires less boilerplate.

If you are working with newtypes, you'll have to convert them into Branded types. Indeed, both of these types are not defined in the same way, so they don't interoperate very well. To manage that, we could use some "adapter" to transform a Newtype into a Branded:

// Convert a Newtype<A, B> into a Branded<B, A>
type ToBranded<A> = A extends Newtype<infer Brand, infer UnderlyingType>
  ? t.Branded<UnderlyingType, Brand>
  : never

// extract the brand B from a Branded<A, B>, used to define codecs type guards
type GetBrand<A> = A extends t.Brand<infer Brand> ? Brand : never
Enter fullscreen mode Exit fullscreen mode

Now we can do the following:

import * as NES from 'newtype-ts/lib/NonEmptyString'

type NonEmptyString = ToBranded<NES.NonEmptyString>
const NonEmptyString = t.brand(
  t.string,
  (s: string): s is t.Branded<string, GetBrand<NonEmptyString>> => s.length > 0,
  'NonEmptyString'
)

type String60 = ToBranded<Newtype<{ readonly String60: unique symbol }, string>>
const String60 = t.brand(
  t.string,
  (s: string): s is t.Branded<string, GetBrand<String60>> => s.length <= 60,
  'String60'
)

const NonEmptyString60 = t.intersection([NonEmptyString, String60])
type NonEmptyString60 = t.TypeOf<typeof NonEmptyString60>
// or: type NonEmptyString60 = NonEmptyString & String60
Enter fullscreen mode Exit fullscreen mode

Note: we could also directly define Branded types and completely discard Newtype:

type String60 = t.Branded<string, { readonly String60: unique symbol }>
type NonEmptyString = t.Branded<string, { readonly NonEmptyString: unique symbol }>
type NonEmptyString60 = t.Branded<string, String60 & NonEmptyString>
Enter fullscreen mode Exit fullscreen mode

Hope that helps :)