It's fine if the data model is kept somewhat "atomic" and devs are diligent about actually declaring and documenting types (on my own projects, I'm super diligent about this).
But once types start deriving from types using utility functions and then devs slack and fall back to type inference (because they skip an explicit type), it really starts to unravel because it's very hard to trace fields back to their origin in a _deep_ stack (like 4-5 levels of type indirection; some inferred, some explicit, some derived, some fields get aliased...).
type Dog = {
breed: string
size: "lg" | "md" | "sm"
// ...
}
type DogBreedAndSize = Pick<Dog, "breed" | "size">
function checkDogs(dogs: Dog[]) : DogBreedAndSize[] {
return dogs.map(d => /* ... */)
}
const checkedDoggos = checkDogs([])
Versus: function checkDogs(dogs: Dog[]) {
// ...
}
Very subtle, but for large data models with deep call stacks, the latter is completely unusable and absolutely maddening.