While structural typing is great, it sometimes is desirable to have nominal typing: https://codemix.com/opaque-types-in-javascript/
export type Opaque<Type> = Type & {readonly __opaque__: unique symbol};
Taken from: https://github.com/sindresorhus/type-fest/blob/9ffc7c8a02358c7d0bac1ed62bdf183ed26a0546/source/opaque.d.ts
I think the blog post describes the problem and the solution pretty well.
I think it's named Type in this package
PD: Thanks for the blog post :smile:
Thanks @tonivj5, it's definitely that :taco:
Hey @jhnns, I have a slightly different implementation. It's closer to the one from that nice blog article you linked. And the reason why I chose to pass an Id instead of unique is that it is actually more type-safe:
import {A} from 'ts-toolbelt'
type EUR = A.Type<number, 'eur'>
type USD = A.Type<number, 'usd'>
let eurWallet = 10 as EUR
let usdWallet = 15 as USD
eurWallet = usdWallet // error (unique, would let this pass)
I'm marking this as a question, for the wiki.
Thanks for clarifying @pirix-gh 馃憤. I'm a little bit surprised that one unique symbol is assignable to another unique symbol. Just being curious: do you know why? :)
Well, (from my experience) I think this happens because that symbol is not tied to any variable (but a type instead). The type describes that it can be a unique symbol so it can be any Symbol you'd like.
So if you wanted a really unique symbol, you'd have to do:
declare const Foo: unique symbol;
type uniqueSymbol = typeof Foo;
Which is not something that we can do dynamically. This is why I chose this implementation instead :)
Cheers
Makes sense 馃憤. Picking an arbitrary id from a global namespace is not an ideal solution, but probably the best we can do :)
I just found out that your implementation of an opaque type allows the following:
type Type<A extends any, Id extends string> = A & {
__type__?: Id;
};
type EUR = Type<number, 'eur'>
type USD = Type<number, 'usd'>
let eurWallet: EUR = 10 // is allowed, but should be an error
let usdWallet: USD = 15 // is allowed, but should be an error
This is because __type__ is optional. If it was mandatory, you'd need an assertion to narrow down a number to an EUR.
Maybe we're talking about different use cases of opaque types, but this is a thing I want to prevent in my case. Imagine the following function:
type Iban = Type<string, 'iban'>;
function doTransaction(iban: Iban) {
}
I want to prevent people from calling doTransaction() with an arbitrary string. What I want is something like this:
function parseIban(str: string): Iban {
// do IBAN validation
// throw if invalid
return str as Iban;
}
doTransaction(parseIban("123"));
What do you think? Should the current implementation be changed or should there be another opaque type? 馃榿
Personally, I would even prefer to use a declare const ... as you'd described, like this:
type OpaqueType<Type, TypeSymbol extends symbol> = Type & {__type__: TypeSymbol};
declare const IsoTimestampSymbol: unique symbol;
export type IsoTimestamp = OpaqueType<string, typeof IsoTimestampSymbol>;
This way I can avoid the global string namespace. But you could make it optional of course:
type OpaqueType<Type, TypeSymbol extends symbol | string> = Type & {__type__: TypeSymbol};
Hey @jhnns, thanks for pointing this out. This is a regression, I just changed it recently late at night. It's a mistake. I integrated your suggestion this way:
export type Type<A extends any, Id extends Key> =
A & {__type__: Id}
declare const crypto: unique symbol
type BTC = Type<number, typeof crypto>
type EUR = Type<number, 'eur'>
type USD = Type<number, 'usd'>
let btcWallet = 0.1 as BTC
let eurWallet = 10 as EUR
let usdWallet = 15 as USD
eurWallet = btcWallet // errors again, works
The fix will be out within 5 minutes. Again, sorry about this and thanks for reaching out.
Most helpful comment
Thanks @tonivj5, it's definitely that :taco:
Hey @jhnns, I have a slightly different implementation. It's closer to the one from that nice blog article you linked. And the reason why I chose to pass an
Idinstead ofuniqueis that it is actually more type-safe:I'm marking this as a question, for the wiki.