> For your `type` vs `interface` example, is the fear that you won't be able to use a type when a function takes a `Record` type? It's an inconvenience for sure, but that won't inadvertently introduce a bug.
Right. That one, by itself, won't introduce a bug. But it's just another weird thing to remember about TypeScript's type system being inconsistent. I bumped in to it because I wanted to define my own JSON-like type for use as query params being sent to a particular API that has a custom syntax for certain things (like passing an object as a query param, etc). So I wrote a type something like this:
type ExtendedJson = boolean | number | string | null | undefined | OurCustomTimestampType | SomeOtherCustomTypeWeUse | ExtendedJson[] | ExtendedJsonObject
type ExtendedJsonObject = Record<string, ExtendedJson>
And then in my function that sends requests to the API in question, I require a parameter like `queryParams?: ExtendedJsonObject`, and I have a function that knows exactly how to format this ExtendedJsonObject into the right string for the request. Neato.
Except then, I realized that most of the specific query types I wrote were interfaces. But I only got compile errors in some places and not others. I had no idea why some of my types worked and some didn't. The error message was clear enough, I guess. It said that the type didn't conform to `{ [string]: ExtendedJson }`, but I coudln't figure out why some of my types did conform and some didn't.
What's worse is that some of my query types that were defined as interfaces DID work without a compile error! In hindsight, I now understand that the reason those worked was because I was actually "wrapping" those interfaces into types with things like Readonly<FooQueryParams> or Omit<BarQueryParams, 'someKey'>. So those were now types instead of interfaces, but it wasn't an obvious enough clue for me, so I spent hours trying to figure it all out.
If that were the only weird thing, I wouldn't still be bitching about it, but in the last year that I've been focusing on this frontend project, I feel like I've spent countless hours learning about holes and inconsistencies in TypeScript. It feels like every single week I learn about some other broken or inconsistent nonsense that I just think I shouldn't have to deal with.
> And for your 'inheritance sub-typing' example, can you explain what the "right" behavior would be? It's a function that mutates properties on an object, how is the type system supposed to detect that? That's already a code smell and should be avoided.
The correct behavior is that it should be an error to pass a HasDog to a function that requires a HasAnimal. { pet: Dog } is NOT a sub-type of { pet: Animal } just because Dog is a sub-type of Animal. The type theory concept is called "variance". You have "covariant", "contravariant", and "invariant". Mutable object and array types are invariant in their field types. So you can't pass a Array<Dog> for an Array<Animal> because adding an Animal to an Array<Dog> is a type error: an Animal is not necessarily a Dog. However, you CAN pass an immutable Array<Dog> for an Array<Animal> because you are guaranteed to only be reading the elements as Animals, and a Dog is an Animal. Return types often have the inverse type relationships to input params: You can return an Array<Dog> for an Array<Animal> return type because the caller who receives the returned value will treat it as an Array<Animal>, so it's okay to add a Cat to it, even though it was originally only Dogs.
And, while I agree that mutating an input param is generally a bad idea, that's no excuse for the type system to not be correct. Every other statically typed language is going to handle this correctly (except primitive arrays in Java. They are also unsound, like TS, but, in Java's defense, it's only primitive arrays, and you almost never use arrays directly in Java- you use List<T>, which is not wrong/unsound). When I say every other statically typed language, I'm literally talking about every statically typed language I've ever used: C++, Java (again, except arrays), Go, Swift, Rust, Kotlin, Scala, even PHP. They all handle this correctly, because it's really basic type theory.