The type system can be understandable to normies while the implementation is not. I would rather have a type system that is correct and consistent, with the complexity hidden away, than to have to know where the boundary is in the 80/20 system, which seems like the perfect place for bugs to enter your code.
Agreed. Consistent is WAY better for me than inconsistent. One of the reasons I hate TypeScript so much is that it provides type safety except when it doesn't, and it takes a lot of experience to actually track down the MANY (non-obvious) places where it doesn't.
I could provide a novel, but I'll try to keep it to just a few.
Class methods have incorrect variance, even with the 'strictFunctionTypes' setting: https://www.typescriptlang.org/tsconfig#strictFunctionTypes. What's even more insane is that interface/type methods will have different variance depending on the syntax used to write them:
type SafeFoo = {
// This will have correct type variance
foo: (x: string | number) => void
}
type UnsafeFoo = {
// This will have incorrect type variance
foo(x: string | number): void
}
The `readonly` feature doesn't fully work for object types:
type Foo = {
foo: number
}
type ReadonlyFoo = {
readonly foo: number
}
function useFoo(f: ReadonlyFoo) {
// f.foo = 4 // compile error
const f1: Foo = f
f1.foo = 4 // trololol, I just mutated the input even though I promised it was readonly
}
Classes are kind-of also an interface, which leads to inconsistent/surprising behavior:
class Foo {}
function useFoo(f: Foo) {
if (f instanceof Foo) {
console.log('Cool, a Foo.')
} else {
console.log('Uh, wut?')
}
}
useFoo(new Foo()) // prints: 'Cool, a Foo.'
useFoo({}) // prints: 'Uh, wut?'
"TypeScript is structurally typed"... except when it's not. In my above example with the class pseudo-interface, you can "fix" it by including a private field in the class:
class Foo {
#nominal: undefined
}
function useFoo(f: Foo) {
if (f instanceof Foo) {
console.log('Cool, a Foo.')
} else {
console.log('Uh, wut?')
}
}
useFoo(new Foo()) // prints: 'Cool, a Foo.'
useFoo({}) // This is now a compile error!
Record types are incorrectly typed/handled. A Record<string, string> implies that for ANY string key, there will be a string value. What you almost always actually mean is: Record<string, string | undefined>. Yes, there is a compiler setting (https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAc...) to make all array and record accesses treated as possibly undefined, but that should only be necessary for arrays. The Array type has only one type parameter: the codomain. It has no way of communicating what the domain is (what indexes have values). A Record, on the other hand, DOES allow us to specify both the domain and codomain, so when I write Record<string, string> I'm telling the type system that every string key will be mapped to a string value. Therefore, it should not allow me to assign something that clearly cannot fulfill that contract to it, but it does:
const x: Record<string, string> = {}
In a type system sense, Record<K, V> is more-or-less equivalent to a function type: (K) => V. Imagine if TypeScript handled actual functions similarly:
const x: (s: string) => string = (s) => {}
There's more, but like I said, I don't want to spend all day typing...
Wow, thanks for providing those examples! I’ve been using TypeScript for a while and haven’t ever taken care to notice when the type system is failing me.