Ts-toolbelt: How to create opaque types?

Created on 28 Dec 2019  路  8Comments  路  Source: millsp/ts-toolbelt

馃崺 Feature Request

Is your feature request related to a problem?

While structural typing is great, it sometimes is desirable to have nominal typing: https://codemix.com/opaque-types-in-javascript/

Describe the solution you'd like

export type Opaque<Type> = Type & {readonly __opaque__: unique symbol};

Taken from: https://github.com/sindresorhus/type-fest/blob/9ffc7c8a02358c7d0bac1ed62bdf183ed26a0546/source/opaque.d.ts

Describe alternatives you've considered

Teachability, Documentation, Adoption, Migration Strategy

I think the blog post describes the problem and the solution pretty well.

question wiki

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 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.

All 8 comments

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.

Was this page helpful?
0 / 5 - 0 ratings