question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Discriminated Unions - Multiple Fields, SubClassing/interfacings, kind field detection failure

See original GitHub issue

TypeScript Version: typescript@3.2.0-dev.20181004 Search Terms: Discriminated Unions - Multiple Fields

Issues:

When there are multiple common fields between interfaces the Discriminated Unions is not able to determine which field is to be the discriminator and all the logic that typescript will constrain the shape of the function interfaces bases on a field, check will not work. It would be great if we could in cases like these define a common interface on which there is a single field present from which all other instance inherited from that shape. Then when one defines a union of types, if it can’t find a single common field, then it inspects the base interfaces for an interface with a single field, if found, it will use that field from that interface. Seem to be present with many cases, were I need to communicate to typescript which field should be used.

There would still be a problem, if there is common class from which all inherited that only has a single field, in which case another form of syntax would be required when defined the discriminant, like including the common interface in the discriminate. This method, might be the preferable option, because it would be more performance than inspective all the interfaces form which a class extends from. I would choose this form, because simpler, as know exactly what to inspect at a top level. See examples below of all the cases. type Kinds = KindA | KindB | Kind

An improvement, would be to allow instanceof and similar variants to be used, versus string literals.


// just the common shape.
interface Kind {
    field : any
}
// or more specific
interface Kind {
    field : 'KindA' | 'KindB' // or any, should be good enough.
}

interface KindA extends Kind {
    field : 'KindA'
    fieldCommon : string;
    fieldA : 'KindAFieldA'
}

interface SubKindA extends KindA
{
    subFieldA : 'SubFieldKindA'
}

interface KindB extends Kind {
    field : 'KindB' 
    fieldCommon : string;
    fieldB : 'KindBFieldB'
}

interface SubKindB extends KindB {
    subFieldB : 'SubFieldKindB'
}


type Kinds = KindA | KindB | Kind // Is the other way to specify which interface the single field discriminator is defined in.

type Kinds = KindA | KindB
const kind = {} as Kinds;

switch(kind.field) // On field should be available, fieldCommon must not be.
{
    case 'KindA':
    kind.fieldCommon    
    kind.field
    kind.fieldA

    case 'KindB':
    kind.fieldCommon    
    kind.field
    kind.fieldB // Field should be available, but is missing
}

type SubKinds = SubKindA | SubKindB | Kind // Is the other way to specify which interface the single field discriminator is defined in.

type SubKinds = SubKindA | SubKindB
const subKind = {} as SubKinds;

// expect only fields with literals, should be available here, fieldCommon must not be
switch(subKind.field)
{
    case 'KindA':
    {
    subKind.field      
    subKind.fieldCommon 
    subKind.fieldA   
    subKind.subFieldA; 
    }

    case 'KindB':
    {
    subKind.field
    subKind.fieldCommon;   
    subKind.fieldB;          // should be available here, but is missing
    subKind.subFieldB;      // should be available here, but is missing
    }
}

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:1
  • Comments:5

github_iconTop GitHub Comments

3reactions
blixtcommented, Nov 26, 2018

This issue is a bit hard to get into, maybe it could be written to gently get into the more deep issues? Am I correct in assuming that the root of many of these failures is simply described as the following?

type T1 = { a: number, b: number }
type T2 = { a: number, c: number }
type U = T1 | T2
const x: U = { a: 1, b: 2, c: 3 } // Does not implement either of the union'ed types!

In the above x should not be a valid value for U since it’s not a valid value for T1 nor T2.

And for a more concrete use case: Imagine specifying an options object for a function (so it generally cannot have any literal values to use for discriminating the types) that must include one of two options, but never both. I haven’t figured out a way to do this.

0reactions
donabramscommented, Nov 28, 2018

There’s a few different ways to think about these operations:

Operators as bitwise ops and types as Rows: A | B == Row(A) | Row(B) == Row(A) ∪ Row(B) ex: { a: 1 } | { b: 2 } is satisfied by { a: 1, b: 2 } A & B == Row(A) & Row(B) == Row(A) ∩ Row(B) ex: { a: 1, b: 2 } & { b: 2, c: 3} is satisfied by { b: 2 }

Operators as logical and types as validators: A | B == is(A) || is(B) ex: { a: 1 } | { b: 2 } is satisfied by { a: 1} or { b: 2 } but not { a: 1, b: 2} A & B == is(A) && is(B) ex: { a: 1, b: 2 } & { b: 2, c: 3} is satisfied by { a: 1, b: 2} or { b: 2, a: 3 } but not { a: 1, b: 2, c: 3}

Honestly I’d like both of these AND have types closed by default (otherwise unions are pretty silly). I’d say || should be the latter and | should be the former. Also, XOR is super nice (and use ^ or ^^ for that).

In the end (probably including things outside the scope of this bug), I’d love to be able to describe types like these:

type Omit<K, T extends Object> = { [O in (keyof T ^ K & keyof T)]: T[O] };
type Fluent<R, F extends Function> =  (...argumentsOf F) => R;
type LazyFetch<F extends Function,E> (timeout: Number) => (...argumentsOf F) => Promise<Return<F>, E>;
type IsEqual<A, B> = A extends B ? B extends A ? true : false : false;
type FilterListTypes<L, T> = L extends [infer U] ? U && T : never;
type Transition<S1, E, S2> = (S1, E) => S2
type TNextStates<T> = T extends Transition<any, any, infer U> ? U : never;
type TEvents<T> = T extends Transition<any, infer U, any> ? U : never;
// Types a function where next state is known when current state and event are given
// Also filters the possible events given an initial state
type Machine<Transitions> =
  Transitions extends [Transition<infer CurrentStates, any, infer NextStates>]
  ? (CurrentState in CurrentStates, 
      // filter events by initial state
      Event in TEvents<FilterListTypes<Transitions, Transition<CurrentState, any, any>>
    ) =>
    // filter next state by initial state and event
    TNextStates<FilterListTypes<Transitions, Transition<CurrentState, Event, any>>>
  : never // list isn't Transitions;
Read more comments on GitHub >

github_iconTop Results From Across the Web

Handbook - Unions and Intersection Types - TypeScript
A union type describes a value that can be one of several types. We use the vertical bar ( | ) to separate...
Read more >
TypeScript discriminated union does not give error for non ...
I expect TS to give an error if I pass a prop that does not exist in that discrimination. Is there way to...
Read more >
The case for Discriminated Union Types with Typescript
Discriminated Unions combine more than one technique and create self-contained types. Types that carry all the information to use them without ...
Read more >
Discriminated Unions - TypeScript Deep Dive - Gitbook
If you have a class with a literal member then you can use that property to discriminate between union members. ... type Shape...
Read more >
Unions - The Rust Reference
The union can be accessed using the same syntax as struct fields: ... Reading a union field reads the bits of the union...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found