danvk / effective-typescript

Effective TypeScript 2nd Edition: 83 Specific Ways to Improve Your TypeScript
https://effectivetypescript.com
Other
1.53k stars 226 forks source link

Wrong sentence in page 72? #27

Closed xddq closed 1 year ago

xddq commented 1 year ago

Hey @danvk !

Thanks for the great book, just skimming through it, but already gotten great advice! I feel like there is a small error on page 72 (or I am to dumb? sorry if thats the case). It says:

number[] is a subtype of readonly number[] The sentence directly after says (Its easy to get this backwards- remember Item 7!)

I don't get how number[] is supposed to be a set with less possible values than readonly number[]?

readonly number[] lacks push, pop, etc..

I think the correct sentence should be readonly number[] is a subtype of number[]

danvk commented 1 year ago

The book is correct. But you're not too dumb! This is, indeed, easy to get backwards :)

Type A is a subtype of type B if all values assignable to A are also assignable to B. Similarly, type A is not a subtype of type B if there's a value assignable to A that is not assignable to B.

In other words, with the right value, we can sort out the subtype relationship.

Here's some code (playground):

{
    const a: readonly number[] = [1, 2, 3];
    const b: number[] = a;
    //    ~ The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
}

{
    const a: number[] = [1, 2, 3];
    const b: readonly number[] = a;  // ok
}

As you can see from the first block, the readonly number[] value is not assignable to number[]. So readonly number[] is not a subtype of number[].

From the second block, you can see that number[] is assignable to readonly number[]. So number[] is a subtype of readonly number[].

Perhaps a more intuitive way to think of this is to imagine a value x that has all the usual array methods like map, filter, etc., except it's missing push and pop. This value is assignable to readonly T[] (which does not require push and pop) so it's in the domain of readonly T[]. But it is not assignable to T[] (which does require push and pop) so it's not in the domain of T[]. If you were to draw a Venn diagram, the domain of readonly T[] would be a big circle with a smaller circle for T[] entirely inside of it. The hypothetical value x would be in the bigger circle but outside the smaller one.

Hopefully that makes some sense!

xddq commented 1 year ago

Hey @danvk, thanks for the quick response! Sadly, I still don't get it. You don't have to bother answering once more. I just write this down to be able to refer to my question when asking friends or elsewhere. Just to mention what I don't get. You said:

"If you were to draw a Venn diagram, the domain of readonly T[] would be a big circle with a smaller circle for T[] entirely inside of it."

...but how can T[] be entirely inside of readonly T[] if it contains values (e.g. push and pop methods) that are not even part of readonly T[]?

My impression

Is that we can assign a value of type T[] to readonly T[] since Typescript uses structural typing and T[] contains the attributes that are required to fit into readonly T[].

However, when drawing the Venn diagram to me it feels like it should look somewhat like this: image

Where I would say that readonly T[] is a subtype of T[] (all values of readonly T[] live inside T[] in the Venn diagram).

Edit

To me the given example with readonly T[] and T[] is equivalent to something like this playgroundlink:

const x: {a: number, b: string} = {a: 1, b: "hello"}
const y: {a: number} = x
// This works. For me this is exactly the same as assigning a number[] to a readonly number[]
// However, I would not claim that the type {a: number, b:string} is a subtype of {a: number}

const z: {a:number} = {a: 1, b: "hello"}
// Type '{ a: number; b: string; }' is not assignable to type '{ a: number; }'.
//Object literal may only specify known properties, and 'b' does not exist in type '{ a: number; }'.(2322)

Here I would (same as before) not claim that {a: number, b:string} is a subtype of {a:number}.

xddq commented 1 year ago

Okay I think I finally understood your point.

The "subtype" and "subset" terms are based on the knowledge that Typescript is structurally typed.

If we say we have the type

type T = {
x: number
}

We have an inifinite amount of possible values that can match that type. For example:

{
x: 1
}

{
x: 2
}

but (more importanly) also

{
x: 2,
y: "hello"
}

and

{
x: 1,
y: "hello",
z: 10
}

Now lets say we have type U with:


type U = {
x: number,
y: string
}

which would also have an infinite amount of possible values. But it does not have possible values where we have


{
x: 1
}

{
x: 2
}

so therefore in the Venn diagram the type of T would be the outer circle and the type of U would be the inner circle (all values of U are inside of the outer circle). The values in the outer circle that are not inside of the inner circle are the ones that are an object with a single attribute "x" of type number.

danvk commented 1 year ago

I think you've figured it out! This has everything to do with structural typing.

...but how can T[] be entirely inside of readonly T[] if it contains values (e.g. push and pop methods) that are not even part of readonly T[]?

push and pop are not values. They're methods/properties on a value. The values in this diagram would be things like [], [1, 2, 3], [0, 'x'], etc.

I would not claim that the type {a: number, b:string} is a subtype of {a: number}

I would :) We can walk through it if it's helpful, but based on your last comment I think you've got it. The key is that, thanks to structural typing, a value like {a: 1, b: 2} is assignable to {a: number}, even though that type doesn't say anything about a b property.