io-ts icon indicating copy to clipboard operation
io-ts copied to clipboard

Describe smart constructor for objects

Open steida opened this issue 4 years ago • 6 comments

📖 Documentation

We can enforce business rules stricter than plain types with https://dev.to/gcanti/functional-design-smart-constructors-14nb pattern. We can leverage https://github.com/gcanti/io-ts/blob/master/index.md#branded-types--refinements for that.

But it seems nowhere is described how to brand non-primitive types. Is this pattern right or am I overlooking something?

// Internal (not exported) type used only for Foo construction.
const _Foo = t.type({
  first: t.string,
  second: t.string,
});
type _Foo = t.TypeOf<typeof _Foo>;

interface FooBrand {
  readonly Foo: unique symbol;
}
export const Foo = t.brand(
  _Foo,
  (n): n is t.Branded<_Foo, FooBrand> => n.first !== n.second, // Just an example of something.
  'Foo',
);
export type Foo = t.TypeOf<typeof Foo>;

steida avatar Oct 25 '20 15:10 steida

OK, this approach is not type-safe. The spread operation on an object can make an invalid type.

@gcanti Any idea? How do you brand objects?

steida avatar Nov 10 '20 20:11 steida

As I see it, branded objects are still useful despite the fact they are easily breakable anytime the original object is somehow reused. They only can not be created from scratch. As a workaround, Foo.encode encodes value to not branded type, so if users encode everything before a manipulation, branded types are safe enough.

steida avatar Nov 10 '20 21:11 steida

What about a private constructor + a private property?

import * as O from 'fp-ts/Option'

class Foo {
  static smartConstructor(first: string, second: string): O.Option<Foo> {
    return first === second ? O.some(new Foo(first, second)) : O.none
  }
  private readonly _: unknown
  private constructor(readonly first: string, readonly second: string) {}
}

declare const foo: Foo

export const foo2: Foo = { ...foo, first: 'a' } // error: Property '_' is missing in type '{ first: string; second: string; }' but required in type 'Foo'.

gcanti avatar Nov 11 '20 08:11 gcanti

@gcanti Thank you, but how to do it with io-ts?

steida avatar Nov 11 '20 13:11 steida

For what is worth, it seems branded Array is safe because there is no spread operation which would copy brand prop. So we can at least enforce sorted etc. arrays safely.

const _Foo = t.readonlyArray(t.Int);
type _Foo = t.TypeOf<typeof _Foo>;

interface FooBrand {
  readonly Foo: unique symbol;
}
export const Foo = t.brand(
  _Foo,
  (n): n is t.Branded<_Foo, FooBrand> => n.length > 2, // Just an example of something.
  'Foo',
);
export type Foo = t.TypeOf<typeof Foo>;

const f = Foo.decode([1, 2, 3]);
if (f._tag === 'Right') {
  const a: Foo = f.right;
  // Error
  const b: Foo = [...a]
}

steida avatar Nov 28 '20 01:11 steida

It seems objects should be doable as well via Symbol

https://functionalprogramming.slack.com/archives/CPKPCAGP4/p1611993770099300?thread_ts=1611855662.084900&cid=CPKPCAGP4 https://functionalprogramming.slack.com/archives/CPKPCAGP4/p1611869451091300?thread_ts=1611869293.090800&cid=CPKPCAGP4

steida avatar Jan 31 '21 19:01 steida