Open steida opened 4 years ago
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?
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.
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 Thank you, but how to do it with io-ts?
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]
}
📖 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?