简体   繁体   中英

Typescript Nested Tagged/Discriminated Union Types

Let's say I have the following types (syntax is Elm-ish/Haskell-ish):

type Reply = LoginReply | LogoutReply
type LoginReply = LoginSucceeded | AlreadyLoggedIn String

If I try to model it using Typescript's discriminated unions, I'll face the problem that LoginReply needs to have a property kind with value "loginReply" but it can't because it's declared using type keyword, not class .

That's my best shot at the problem till now:

type Reply = LoginReply | LogoutReply

type LoginReply = LoginSucceeded | AlreadyLoggedIn

interface LoginSucceeded {
  kind: "loginSucceeded";
}

interface AlreadyLoggedIn {
  kind: "alreadyLoggedIn";
  loggedInUsername: string;
}

interface LogoutReply {
  kind: "logoutReply";
}

As you can see, one can't even use "loginReply" anywhere and thus can't be used in discrimination.

The only workaround I have for knowing a Reply variable is a LoginReply is to see if its kind is one of "loginSucceeded" and "alreadyLoggedIn" .

So, how do I achieve what I want in Typescript? How to create a discriminated union type whose child types are discriminated-union types themselves?

Since LoginReply is just a union of LoginSucceeded | AlreadyLoggedIn LoginSucceeded | AlreadyLoggedIn , from the type system perspective there is no difference between type Reply = LoginReply | LogoutReply type Reply = LoginReply | LogoutReply and type Reply = LoginSucceeded | AlreadyLoggedIn | LogoutReply type Reply = LoginSucceeded | AlreadyLoggedIn | LogoutReply type Reply = LoginSucceeded | AlreadyLoggedIn | LogoutReply . The type keyword introduces a type alias which as the name suggests is just a convenient name for the type not a new type in itself.

You have two options, neither of them 100% what you want.

You can check all possible values kind of LoginReply as you mentioned you considered:

function doStuff(o: Reply) {
    switch(o.kind)
    {
        case 'alreadyLoggedIn' : 
        case 'loginSucceeded' : 
            o; /* is LoginReply */ break;
        case 'logoutReply': o // o is  LogoutReply
    }
} 

Or you can add an extra subKind field for the LoginReply subtypes:

export type Reply = LoginReply | LogoutReply

type LoginReply = LoginSucceeded | AlreadyLoggedIn

interface LoginSucceeded {
    subKind: "loginSucceeded";
    kind: "loginReply";
}

interface AlreadyLoggedIn {
    kind: "loginReply";
    subKind: "alreadyLoggedIn";
    loggedInUsername: string;
}

interface LogoutReply {
    kind: "logoutReply";
    logout: boolean
}

function doStuff(o: Reply) {
    switch(o.kind)
    {
        case 'loginReply' : o // is LoginReply
            switch(o.subKind) {
                case 'alreadyLoggedIn' : o; /* is AlreadyLoggedIn */ break;
                case 'loginSucceeded' : o; /* is LoginSucceeded */ break;
            }
            break;
        case 'logoutReply': o // LogoutReply
    }
} 

Will keep this answer for all solutions. Please, update if you have another solution.


Solution#1 : As described in the question, see if kind is one of "loginSucceeded" and "alreadyLoggedIn" .


Solution#2 : As described by Titian Cernicova-Dragomir in his answer , use kind and subKind .


Solution#3 :

If one argues that Solution#2 makes LoginSucceeded and AlreadyLoggedIn know too much about them being discriminated types of discriminated types, one could also argue that, even under one level of discrimination, they shouldn't have had knowledge of kind . If one follows that argument, one could implement a solution like this:

type Reply = { kind: "loginReply", t: LoginReply } | { kind: "logoutReply", t: LogoutReply }

type LoginReply = { kind: "loginSucceeded", t: LoginSucceeded } | { kind: "alreadyLoggedIn", t: AlreadyLoggedIn }

interface LoginSucceeded {
}

interface AlreadyLoggedIn {
    loggedInUsername: string;
}

interface LogoutReply {
    logout: boolean
}

function doStuff(o: Reply) {
    switch(o.kind)
    {
        case 'loginReply' : o.t; // is LoginReply
            const loginReply = o.t;
            switch(loginReply.kind) {
                case 'alreadyLoggedIn' : loginReply.t; /* is AlreadyLoggedIn */ break;
                case 'loginSucceeded' : loginReply.t; /* is LoginSucceeded */ break;
            }
            break;
        case 'logoutReply': o.t; // LogoutReply
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM