Hacker News new | ask | show | jobs
by ragnese 1047 days ago
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...
1 comments

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.