Closed grassator closed 6 years ago
@grassator on the typesafety side; great. What's the story on error messages when setting a EUR on a USD? (the previous string scheme was indicative of the error with an helpful message)
Would you be open to support this?
@grassator there no constraint on URI
export interface Newtype<URI, A> {
_URI: URI
_A: A
}
So AFAIK it's already supported.
What's the story on error messages?
@sledorze isn't good
interface EUR extends Newtype<{ readonly tag: unique symbol }, number> {}
interface USD extends Newtype<{ readonly tag: unique symbol }, number> {}
declare function f(eur: EUR): void
declare const eur: EUR
declare const usd: USD
f(eur) // ok
f(usd) // Type 'typeof tag' is not assignable to type 'typeof tag'. Two different types with this name exist, but they are unrelated
However the error message can be improved using different names for the tag
interface EUR extends Newtype<{ readonly EUR: unique symbol }, number> {}
interface USD extends Newtype<{ readonly USD: unique symbol }, number> {}
f(usd) // Property 'EUR' is missing in type '{ readonly USD: unique symbol; }'
And if you define EUR
twice you get
interface EUR2 extends Newtype<{ readonly EUR: unique symbol }, number> {}
declare const eur2: EUR2
f(eur2) // Type 'typeof EUR' is not assignable to type 'typeof EUR'. Two different types with this name exist, but they are unrelated
Using an object for URI
can be helpful to encode relations between newtypes (via subtyping).
Given the following basic types
interface NonZero extends Newtype<{ readonly NonZero: unique symbol }, number> {}
interface NonNegative extends Newtype<{ readonly NonNegative: unique symbol }, number> {}
interface NonPositive extends Newtype<{ readonly NonPositive: unique symbol }, number> {}
interface Integer extends Newtype<{ readonly Integer: unique symbol }, number> {}
One can build more newtypes...
// merges two newtypes
interface Concat<N1 extends Newtype<object, any>, N2 extends Newtype<object, Carrier<N1>>>
extends Newtype<N1['_URI'] & N2['_URI'], Carrier<N1>> {}
interface NonZeroInteger extends Concat<Integer, NonZero> {}
interface Positive extends Concat<NonNegative, NonZero> {}
interface Negative extends Concat<NonPositive, NonZero> {}
interface PositiveInteger extends Concat<Integer, Positive> {}
interface NegativeInteger extends Concat<Integer, Negative> {}
Example
declare function f(x: NonZero): void
declare const i: Integer
declare const nz: NonZero
declare const nzi: NonZeroInteger
declare const nn: NonNegative
declare const np: NonPositive
declare const p: Positive
declare const n: Negative
declare const pi: PositiveInteger
declare const ni: NegativeInteger
f(i) // static error (Property 'NonZero' is missing in type '{ readonly Integer: unique symbol; }')
f(nz) // ok
f(nzi) // ok
f(nn) // static error
f(np) // static error
f(p) // ok
f(n) // ok
f(pi) // ok
f(ni) // ok
...or other useful type alias
type Rational = [Integer, NonZeroInteger]
Finally, since often we are talking about refinements rather than isomorphisms, we could define a prism
helper along with the existing iso
helper
import { Prism } from 'monocle-ts'
const prism = <S extends AnyNewtype>(predicate: Predicate<Carrier<S>>): Prism<Carrier<S>, S> => {
return new Prism(s => (predicate(s) ? some(s) : none), identity)
}
const isNonNegative = (n: number) => n >= 0
const isInteger = (n: number) => n % 1 === 0
const isNonZero = (n: number) => n !== 0
const isNonZeroInteger = (n: number) => isNonZero(n) && isInteger(n)
const isPositive = (n: number) => n > 0
// etc...
const nonNegative = prism<NonNegative>(isNonNegative)
const integer = prism<Integer>(isInteger)
const nonZero = prism<NonZero>(isNonZero)
const nonZeroInteger = prism<NonZeroInteger>(isNonZeroInteger)
const positive = prism<Positive>(isPositive)
// etc...
const x1 = positive.getOption(1) // Some(1)
const x2 = positive.getOption(-1) // None
Blogpost that @gcanti , its too interesting and neat to stay hidden in an issue. Thanks again
@grassator @sledorze if you agree we could add Concat
and prism
to the library. Maybe also those newtypes (Integer
, NonZero
, etc...)
There's another helper which would be handy
interface Extends<N extends AnyNewtype, Tags extends object> extends Newtype<Tags & N['_URI'], Carrier<N>> {}
Usage
interface EUR extends Extends<Integer, { readonly EUR: unique symbol }> {}
@gcanti Thank you for such quick and comprehensive answer and update to the lib! I agree with @sledorze — would be great to have that in a blog post.
The only thing I wonder if it makes sense to make the order of arguments in Extends
and Newtype
consistent:
// ----------------------------------------------v 1st arg
interface EUR extends Newtype<{ readonly EUR: unique symbol }, number> {}
// -----------------------------------------------------v 2nd arg
interface EUR2 extends Extends<Integer, { readonly EUR: unique symbol }> {}
The only thing I wonder if it makes sense to make the order of arguments in Extends and Newtype consistent
@grassator I'm not sure, they are quite different
// v-- this is the carrier
interface EUR extends Newtype<{ readonly EUR: unique symbol }, number> {}
// v-- this is NOT the carrier (the carrier is still number)
interface EUR2 extends Extends<Integer, { readonly EUR: unique symbol }> {}
I haven't a strong opinion on this though
@grassator released https://github.com/gcanti/newtype-ts/releases/tag/0.2.1
Thanks a lot for the suggestion about unique symbol
s.
@gcanti Thanks, new release looks great.
Since version 2.7 it is possible to use
unique symbol
to have truly unique types instead of relying on strings as tags.Would you be open to support this? I could the following API:
It's a bit uglier, but offers increased type safety. With conditional types I believe it could even be possible to support both...