Closed matthewpflueger closed 4 years ago
This is what I'm doing at the moment, it appears to correctly flag if you pass an inappropriate base codec:
const NewtypeDecoderFactory = <T, U>(id: string, iso: Iso<T, U>, baseCodec: t.Type<U>) =>
new t.Type<T, U>(
id,
(u): u is T => baseCodec.is(u),
(u, c) => either.map(baseCodec.validate(u, c), iso.wrap),
iso.unwrap,
);
// [...]
type Seconds = Newtype<{ readonly Seconds: unique symbol }, number>;
const Seconds = iso<Seconds>();
const secondsDecoder = NewtypeDecoderFactory('Seconds', Seconds, t.number);
Thank @SamHH this seems to work quite well I optimized it a little bit so you don't need to provide the iso
instance
export const fromNewtype = <N extends AnyNewtype>(codec: t.Type<CarrierOf<N>>, name = `Newtype<${codec.name}>`) => {
const i = iso<N>()
return new t.Type<N, CarrierOf<N>, unknown>(
name,
(u): u is N => codec.is(u),
(u, c) => E.either.map(codec.validate(u, c), i.wrap),
a => codec.encode(i.unwrap(a))
)
}
It would be nice if we could re-add fromNewtype
to the io-ts-types
cause it is the missing link between io-ts
and newtype-ts
.
@gcanti what are your thoughts? I would prepare a PR.
Re-adding fromNewtype would be awesome! In lu of that, I added the old 0.4.7 fromNewtype to my project (with appropriate attribution of course). Any particular reason that one won't work with io-ts?
@mlegenhausen I thought that branded types were the standard now (being more powerful). Is there a realistic use case for newtypes?
@gcanti I saw branded types and am obviously confused a little on how they replace newtypes. I use newtypes extensively to model my domain and also to verify input at the edges. Can branded types be used outside of io-ts like in simple modeling of the domain?
@gcanti I thaught that at first too but newtype-ts allows to write more strict code. The problem with branded types is that it has the build-in downgradability to the base type.
Example
interface PersonId extends Newtype<{ readonly PersonId: unique symbol }, string> {}
type Name = t.Branded<string, { readonly Name: unique symbol }>
interface Person {
id: PersonId
name: Name
bio: string
}
declare const p1: Person
p1.name === p1.bio // No Error cause there is a common base type
p1.id === p1.bio // Wanted error cause the base type is hidden
You can argue that you simply need to define bio
as branded type to get an error but the world is not perfect where every variable is a branded type especially when you integrate foreign libraries.
I still see the value in using newtypes. If I have e.g. an Auth module encapsulating the retrieval and validation of user tokens (this module would be the only one able to create values of the Token type, and accept Tokens in its signatures), I don't want those tokens to be mixed up with anything else in the rest of the application, i.e. I don't want them to be assignable to whatever the carrier type would be
p1.name === p1.bio // No Error cause there is a common base type
@mlegenhausen I would argue that this is a feature
I don't want them to assignable to whatever the carrier type would be
@giogonzo why? are there any cons?
p.s.
Mind you, I'm not against re-adding fromNewtype
, just interested in any cons of branded types
why? are there any cons?
to continue with my example above, I'd like for instance to have the property that a Token
can't be rendered in JSX in my react app. One might say that this could be done from the "other side", i.e. making JSX only accept e.g. RenderableString
. But sometimes this other side is just too big to update, or you don't have direct control over the typings.
In this scenario I see assignability to the carrier type as a con, while I agree with you that it is a pro in the majority of cases
I would argue that this is a feature
@gcanti of cause this should not be the start to replace branded types at all but newtypes allow to make completely distinct set of types but who am I telling this to :wink:
As @giogonzo says for different purposes it seems reasonable to use branded types and newtypes in the same application they don't exclude each other.
I can provide a PR but I think I will need till next week.
Sorry to comment on an old thread, but it came up in Google when searching around this issue. I just wanted to highlight my use case for preferring newtypes here.
I want to differentiate between pounds and pence in an application and prevent accidental arithmetic between the two.
// Using branded types (I haven't included all the boilerplate for creating them)
type Pounds = number & Brand<PoundsBrand>
type Pence = number & Brand<PenceBrand>
const amountInPounds = 30 as Pounds
const amountInPence = 4200 as Pence
amountInPence + amountInPounds // no error, type: number
Because they can be downgraded to number
, the compiler still allows me to mix pounds and pence in arithmetic operations. This is obviously undesirable.
// Using a Newtype prevents this from happening:
interface GBX extends Newtype<{ readonly GBX: unique symbol }, number> {}
const isoGBX = iso<GBX>();
interface GBP extends Newtype<{ readonly GBP: unique symbol }, number> {}
const isoGBP = iso<GBP>();
const amountInPence = isoGBX.wrap(4200)
const amountInPounds = isoGBP.wrap(30)
amountInPence + amountInPounds // error, need to explicitly unwrap types before doing maths.
I'm now forced to be very explicit about how I'm unwrapping the types before doing maths with them.
For Branded or NewTypes you should implement yourself a Field
, Ring
or Semiring
and do the unwrapping there once: https://github.com/gcanti/fp-ts/blob/master/src/Field.ts
🐛 Bug report
Hi, I am trying to upgrade to the latest io-ts and I use a bunch of newtypes (from newtype-ts) with help from io-ts-types like below:
Current Behavior
Declare a newtype:
Then I use it like:
Only the io-ts-types/lib/newtype-ts is missing from the latest version of that library which used to do the conversion.
So how do you use newtypes in io-ts now?
Expected behavior
Be able to use newtypes in io-ts...