What has always bothered me about TypeScript are union types. If you have a function that receives a parameter such as ‘Dog | Cat’, you cannot separate it. For example:

type Dog = { bark: () => void }

type Cat = { meow: () => void }

function speak(animal: Dog | Cat) {

    if (‘bark’ in animal) {

        animal.bark();

    } else {
        animal.meow();
    }
}

Okay, okay, I know you can filter using ‘in’ to see if it has methods, but in real life, in a company where you have a colleague (who is a golden boy) who writes over-engineered code with hundreds of interfaces of interfaces, you don’t want to spend time searching through the files to find every element that is in the union type.

Whereas in Rust it does:

struct Dog { name: String, }

struct Cat { name: String, }

enum Animal {

    Dog(Dog),

    Cat(Cat),
}

fn process_animal(animal: Animal) {

    match animal {

        Animal::Dog(dog) => {

            println!(‘It is a dog named {}’, dog.name);

        }

        Animal::Cat(cat) => {

            println!(‘It is a cat named {}’, cat.name);

        }
    }
}

I think TypeScript should add a couple of lines of code to the generated JavaScript to do something like:

type Dog = { bark: () => void }

type Cat = { meow: () => void }

function speak(animal: Dog | Cat) {

    if (animal is Dog) {

        animal.bark();

    } else {

        animal.meow();

    }
}

The idiomatic way to do this in TypeScript is with discriminated unions. You’re basically just giving the type system an extra property that makes it trivial to infer a type guard (while also making the runtime check in the compiled JavaScript foolproof).

This does act exactly as a discriminated union. The code works exactly as written.

You're looking for a discriminated union [1], which idiomatically:

  type Dog = { bark(): void; type: 'dog' }

  type Cat = { meow(): void; type: 'cat' }

  function speak(animal: Dog | Cat) {
    if (animal.type === 'dog') {
      animal.bark()
    } else {
      animal.meow()
    }
  }
Generally speaking, TypeScript does not add runtime features.

TypeScript checks your use of JavaScript runtime features.

[1] https://www.convex.dev/typescript/advanced/type-operators-ma...

Your first code block works exactly as you would expect and has been working like that for many years

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAIg9gcyg...

The op did say they didn't want to do these type of checks.

I thought the answer was 'instanceof'?

https://www.typescriptlang.org/docs/handbook/2/narrowing.htm...

I see what you mean, thanks. instanceof works if you're using javascript classes but not for "types".

You can't do `instanceof Dog`. `instanceof` is a JavaScript feature

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Typeguard is what you are looking for: function isDog(animal: Dog | Cat): animal is Dog { return "bark" in Dog }

Then: isDog(animal) ? animal.bark() : animal.meow() You get full type narrowing inside conditionals using typeguards.

You don't even need that. The code exactly as presented acts as a discriminator. TypeScript is smart enough to handle that logic in the if block and know whether animal has been validated as Dog vs Cat. GP is complaining about a feature that already exists in TypeScript

I’m not at my computer so I can’t remember the exact behavior of this situation, but was OP more so referring to autocomplete abilities of typescript? I think they were saying, you first must know if the object barks or meows, you must first type that in in order to get the benefit of type checking and subsequent autocomplete conditional body, which is annoying when you are dealing with complicated types. It requires you to do some hunting in to the types, rather than using a piece of code more like an interface.

It depends how you construct Dog and Cat. With Javascripts dynamic prototype chain, you could never know for sure.

Try it

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAIg9gcyg...

type Mutt = Dog & Cat

const imposter: Mutt = { bark: () => console.log("woof"), meow: () => console.log("meow"), }

You're both misunderstanding parent's point as well as the original point. Nobody ever claimed your link wouldn't compile.

I see what you mean, thanks

Well imo GP is fundamentally misunderstanding TypeScript. It's explicitly a structural language not a nominal one. It goes against the entire design philosophy of TS

It would have been a super reasonable reply to talk about the history of TypeScript, why fundamentally its types exist to retroactively describe complicated datastructures encountered in real world JavaScript. And why when TypeScript overstepped that by creating enums, which require code generation and not mere type erasure to compile, it was decided to be a mistake that they won't repeat.

But instead your rebuttal was pointing out that TypeScript can compile OP's example code, which OP presented as valid TypeScript that they disliked. I'm not defending their position, I'm just saying that it didn't appear you had even properly read their comment.

[deleted]

You're talking about adding a runtime feature. TypeScript doesn't do that anymore. It can't control what properties are on objects or add new ones - you do that yourself in the standard JavaScript portion of the language. TypeScript only lets you describe what's there.

As a sibling said, discriminated unions are they way to go here. You can also add custom type guard functions if you can't control the objects but you want to centralize the detection of the types, but it's better to let TypeScript do it itself so that you don't mess something up with a cast.

And then you have a

    const CatDog = { bark(){}, meow(){} }
and

    const TreeCat = { bark: "oak", meow(){} }
and your code stops working

To make types discriminatable you need either

    type A = { bark: fn, meow?: never } | { bark?: never, meow: fn }
    type B = { species: "dog", bark: fn } | { species: "cat", meow: fn }
    or use instanceof with a class

That's intentional, TS types are erased in runtime. My go-to way for this is discriminated unions: https://www.typescriptlang.org/docs/handbook/unions-and-inte...

What always bothers me with enums, sealed-types, etc is that I can't compose a new ad-hoc set based on elements of someone else's enum. You can make one using the other but not the other way around, TypeScript's is more general.

You can literally do what your generated example does using a type guard. You can also use method overloaded signatures if you dont want to expose your API consumers to union types.

You need a tagged union for this in typescript.

this post on union types versus sum types is worth a read (the tl;dr is that they both have their uses and one is not strictly better) https://viralinstruction.com/posts/uniontypes/