19269

Extracting from a Union Type when some have identical shapes / properties

<h3>Question</h3>

I am trying to build an implementation of a CQRS-style command bus, where the interface to the command bus is a single function, dispatch:

<pre class="lang-js prettyprint-override">const result = dispatch(message)

The dispatch function's type signature is something like this:

<pre class="lang-js prettyprint-override">type Dispatch<Message, Result> = (message: Message) => Result

Imagine, for example, that we want to provide an interface to a Git repo. Some of the messages might be:

<pre class="lang-js prettyprint-override">class Clone { constructor(public readonly remoteUrl: string) { } } class Checkout { constructor(public readonly branchName: string) { } } class RevParse { constructor(public readonly branchName: string) { } }

For each Message, there is a known type of Result. After some experimentation, I think the right way to express that "protocol" is like this:

<pre class="lang-js prettyprint-override">type Protocol = [Clone, void] | [Checkout, void] | [RevParse, string]

The Protocol is a union of tuple types, each expressing the relationship between a Message and an expected type of Result. In the example, only RevParse should be expected to return anything interesting - the others just return void.

To be able to figure out the expected Result for a given Message, I had learned that I can use the Extract utility type, like this:

<pre class="lang-js prettyprint-override">type Result<Message> = Extract<Protocol, [Message, any]>[1] type Dispatch<Message extends Protocol[0]> = (message: Message) => Result<Message>

However, I've discovered that this seems to fail when two of the Messages have the same properties. For example, I can return a string from the Checkout message. I'm assuming this is because Extract matches both Checkout and RevParse when given the Checkout type to look up the right Result, since both types look like { branchName: string}.

<pre class="lang-js prettyprint-override">// should fail with type error because the protocol says Checkout should return void. const checkoutResult: Result<Checkout> = 'string' // const checkoutResult: string | void

I have other questions about this problem but first I need to understand the right way to express the relationship between the Message and Result types. Are my assumptions about the Result lookup correct? Should I be doing completely something different than using the union of tuples? Do I need to add some property to each Message to uniquely identify it? Something else?

Playground Link


<h3>Answer1:</h3>

I don't really know the use cases and the context of what you are trying to build, but I feel like dispatch returning different types is not a good practice here (there are some cases where it can be necessary : https://softwareengineering.stackexchange.com/questions/225682/is-it-a-bad-idea-to-return-different-data-types-from-a-single-function-in-a-dyna ). You could use Strategy Pattern.

Anyway, let's say this use case is legit :

As you mentionned it, Extract matches both Checkout and RevParse when Checkout is given to Result. Indeed, Typescript doc says :

<blockquote>

Extract<T,U> Constructs a type by extracting from T all properties that are assignable to U

</blockquote>

In your case, [Checkout, void] and [RevParse, string] are assignable to [Checkout, any] (when you do Result) It means Checkout and RevParse is assignable to Checkout, and void & string are assignable to any.

The reason is that for classes, Typescript uses structural typing as the following, according to the documentation :

<blockquote>

they (classes) have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static members and constructors do not affect compatibility.

</blockquote>

On the contrary

<blockquote>

Private and protected members in a class affect their compatibility.

</blockquote>

Therefore, all that matters is the structure of a type, not the name of a type. If two types are structurally equivalent they are interchangeable. If you don't want that to happen, you can use "nominal typing". There are several approaches, although I think it should be use exceptionally as for now it is not yet native in Typescript. There is a current PR so it might become native in TS soon, using "unique" keyword. For now :

<ol><li>You can add private property to your classes to make it different, even if they have a different name.</li> </ol><blockquote>

</blockquote> class Clone { private __nominal: void; constructor(public readonly remoteUrl: string) { } } class Checkout { private __nominal: void; constructor(public readonly branchName: string) { } } class RevParse { private __nominal: void; constructor(public readonly branchName: string) { } } <ol start="2"><li>You can use static property with different names, using "Brand" suffix, as proposed and used by Typescript team</li> </ol><blockquote>

</blockquote> class Clone { _cloneBrand: any; constructor(public readonly remoteUrl: string) { } } class Checkout { _checkoutBrand: any; constructor(public readonly branchName: string) { } } class RevParse { _revParseBrand: any; constructor(public readonly branchName: string) { } }

This, will fix your second issue, it will become :

// const checkoutResult: void

For the dispatch function, you should just do the following, using your Result type already defined :

const dispatch = <Message extends Protocol[0]>(message: Message): Result<Message> => { if (message instanceof Clone) { // do clone stuff return } if (message instanceof Checkout) { // do checkout stuff // should insist that I return void here return 'should not be allowed' } if (message instanceof RevParse) { const { branchName } = message // do revparse stuff return 'abcdef1234' } throw new Error(`What is this? ${message}`) }

This will fix your first issue, typescript will now consider the return of dispatch(new Clone('url')) as void

来源:https://stackoverflow.com/questions/62151019/extracting-from-a-union-type-when-some-have-identical-shapes-properties

Recommend

  • Extracting from a Union Type when some have identical shapes / properties
  • For binary files should I use bfiles or bigfiles?
  • Create HTTPS client in NodeJS
  • Why is Calendar returning the wrong hour with the correct time-zone?
  • GET requests in TOR network without installing TOR browser/bundle?
  • Since TranslateMessage() returns nonzero unconditionally, how can I tell, either before or after the
  • Can we implement some code that fires upon selecting something in google document?
  • How to read a pdf file stored in sdcard using adobe reader installed in the android device?
  • how to use an array of textures in metal
  • Angular 4 + Universal : Has no exported member 'StaticProvider'
  • Initializing a pointer to compound literals in C
  • Running ASP.NET Web Api 2 application without Visual Studio
  • Download local file in angularJS
  • Is there a modern ( e.g. CLR ) replacement for bison / yacc?
  • hover link to change *PAGE* background color with css
  • Select multiple fields with single group by in django
  • Search for text in a string, copy & paste rows to new sheet
  • How to efficiently work with multiple database tables in Ruby on Rails
  • how java graphics repaint method actually works
  • When scaling and drawing an image to canvas in iOS Safari, width is correct but height is squished
  • Is it possible to add a hyperlink to a UIAlertController?
  • Creating and managing two independent random number sequences
  • QNetworkAccessManager one instance and connecting slots
  • Python C binding error
  • DocuSign API Replace template document but keep fields
  • List using with references, changes behavior when used as a member
  • Implementing and using MinMax with four in row (connect4) game
  • How to get WinForms custom control's default value to be respected when first dropped on a form
  • How to debug iBeacons and Passbook
  • characters not allowed in DOM ids by spec, and by browser
  • Populating a database table with returned JSON
  • Multiplying polynomials/simplifying like terms
  • using maven pom while creating jar:test-jar some times it says JAR will be empty - no content was ma
  • Stacked bar chart with continuous time-axis as x-axis
  • How do I add a mouse over tooltip to an Image using .DrawImage()
  • Google App Engine Datastore: Dealing with eventual consistency
  • ssh remote server login script
  • Codeigniniter insert data through models and controller
  • How to call different template for different category archive page in woocommerce
  • Cross compile glibc for arm, got undefined reference to some unwind functions