I stumbled into a limitation of structural typing and wondered if there was a way round it.
Suppose I have a hierarchy of types as follows:
type Message = HelloMessage | GoodbyeMessage
type HelloMessage = {
message: string
}
type GoodbyeMessage = {
message: string
}
and we define some type guards as follows:
function isHello(value: any): value is HelloMessage {
return !!value && !!value.message && typeof value.message === "string" && value.message.includes("hello")
}
function isGoodbye(value: any): value is GoodbyeMessage {
return !!value && !!value.message && typeof value.message === "string" && value.message.includes("goodbye")
}
Attempting to use the type guards in a function leads to subsequent uses being typed as never by the compiler:
function someFunc(input: Message): string {
if (isHello(input)) {
return input.message
}
if (isGoodbye(input)) {
return input.message // compiler errors here
}
return "unknown"
}
Some obvious solutions are to type the input as any or cast input as GoodbyeMessage inside the if statement, but neither of these feel particularly elegant.
Is this simply a limitation of Typescript's structural nature of type or is there some other magic that can be incanted to make it work as I'm expecting?
You're right, this is a limitation of the typing system. Even if the type guards worked, you couldn't actually tell if a function was passed
HelloMessageorGoodbyeMessage, since all it sees is{ message: string }. You could instead use a tagged union type, which can be discriminated with type guards: