Type guards for types with the same fields

97 views Asked by At

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?

1

There are 1 answers

1
Szigeti Péter On

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 HelloMessage or GoodbyeMessage, since all it sees is { message: string }. You could instead use a tagged union type, which can be discriminated with type guards:

type Message = HelloMessage | GoodbyeMessage;

type HelloMessage = {
  type: "hello";
  message: string;
}

type GoodbyeMessage = {
  type: "goodbye";
  message: string;
}

// Prefer to use `unknown` for additional type safety.
function isHello(value: unknown): value is HelloMessage {
  return typeof value === "object"
      && value !== null
      && "type" in value
      && typeof value.type === "string"
      && value.type === "hello";
}

function isGoodbye(value: unknown): value is GoodbyeMessage {
  return typeof value === "object"
      && value !== null
      && "type" in value
      && typeof value.type === "string"
      && value.type === "goodbye";
}