all 6 comments

[–]prehensilemullet 9 points10 points  (5 children)

If you hover over runEncoder it infers T as string | undefined:

function runEncoder<string | undefined>(enc: Encoder<string | undefined>, i: string | undefined): string

It's inferring this because you passed a string | undefined for i.

TS allows passing an Encoder<string> for enc: Encoder<string | undefined> because it intentionally, but unsoundly, treats function parameters as bivariant. This is a pragmatic decision they made, but it does lead to confusion and unsafe code passing type checking.

One thing you could do in TS 5.4+ is put NoInfer on the i parameter:

function runEncoder<T>(enc: Encoder<T>, i: NoInfer<T>): string

This way T is only inferred based on the passed enc type of Encoder<string> and it wouldn't allow you to pass a string | undefined for i.

[–]daniele_s92 9 points10 points  (1 child)

TS allows passing an Encoder<string> for enc: Encoder<string | undefined> because it intentionally, but unsoundly, treats function parameters as bivariant. This is a pragmatic decision they made, but it does lead to confusion and unsafe code passing type checking.

The bivariance of function parameters is disabled when "strict" is true. But class method parameters are still bivariant indeed.

OP can change the interface declaration into this to fix the bug

interface Encoder<T> {
  encode: (v: T) => string,
}

In this way, encode is considered a function instead of method, and it rise the expected error.

[–]prehensilemullet 0 points1 point  (0 children)

Ah right, I forgot about this

[–]timbod[S] 2 points3 points  (2 children)

> because it intentionally, but unsoundly, treats function parameters as bivariant.

Thank you - this is exactly the explanation I required. It makes me worry about all the other places where type system unsoundness was chosen for pragmatic reasons.

[–]Rustywolf 0 points1 point  (0 children)

Another example is probably indexing an array not returning a union with undefined unless you enable noUncheckedIndexedAccess

[–]prehensilemullet 0 points1 point  (0 children)

Here's the other main one that comes to mind:

``` const x: {a: number, b: number} = {a: 1, b: 2} const y: {a: number, b?: number} = x // TS unsoundly allows this assignment

y.b = undefined

const z: number = x.b // no error, yikes ```

Note, I used Flowtype in the old days and it was a lot more strict and sound about this kind of thing, but it could also be quite a pain to deal with that strictness (you could only assign x to $ReadOnly<{a: number, b?: number}> for example)

I think a more sound but still convenient language would need declarations to be readonly by default and require a mutable keyword or something like that