-
Notifications
You must be signed in to change notification settings - Fork 0
Chapter 11 #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: initial
Are you sure you want to change the base?
Chapter 11 #11
Conversation
This is because we've told TypeScript that `config` is a `Record` with a any number of string keys. We annotated the variable, but the actual _value_ got discarded. This is an important point - when you annotate a variable, TypeScript will: | ||
|
||
1. Ensure that the value passed to the variable matches the annotation. | ||
2. Forget about the value's type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not always because there is something that internally is called an assignment reduced type:
const test: 1 | 2 = 1;
test;
// ^? const test: 1
|
||
There's a difference in TypeScript between annotating _variables_ and _values_. The way they conflict can be confusing. | ||
|
||
### When You Annotate A Variable, The Variable Wins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
### When You Annotate A Variable, The Variable Wins | |
### When You Annotate A Variable, The Annotation Wins |
|
||
But this isn't really what we want - this is a config object that shouldn't be changed. | ||
|
||
### With No Annotation, The Value Wins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what it "win" with here in such a situation? ;p
### With No Annotation, The Value Wins | |
### With No Annotation, The Inferred Type of a Value Is Used |
|
||
- When you use a variable annotation, the variable's type wins. | ||
- When you don't use a variable annotation, the value's type wins. | ||
- When you use `satisfies`, you can tell TypeScript that a value must satisfy certain criteria, but still allow TypeScript to infer the type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which has some notable surprising caveats at times, like:
type User = { name: string }
const arr = [] satisfies User[]
// ^? const arr: never[]
} satisfies Album; | ||
``` | ||
|
||
Now, `album.format` is inferred as `"Vinyl"`, because we've told TypeScript that `album` satisfies the `Album` type. So, `satisfies` is narrowing down the value of `album.format` to a specific type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more technical terms would be that the satisfies
operator provides contextual information that helps the inferred value type to retain the literal string type here as otherwise it would be widened to a string.
This is the same mechanism as with:
const album: Album = {
format: "Vinyl",
};
Both of them provide the contextual type for that expression and it's what makes the literal string type to be retained in both of those examples
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I'm perhaps picky about here is that I wouldn't call it narrowing here. TS speaks of narrowing in its control flow analysis - when it narrow a union type to one of its members in a branch etc
const id = <string>searchParams.get("id"); | ||
``` | ||
|
||
This is less common than `as`, but behaves exactly the same way. `as` is more common, so it's better to use that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it also just doesn't work in .tsx
files (nor in .cts
or .mts
). I feel like you can mention this as an artifact of the past that people might see in some older codebases but strongly recommend that as
is the way to go and the only way that should be used nowadays
|
||
#### The Limits of `as` | ||
|
||
`as` has some limits on how it can be used. It can't be used to convert between unrelated types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can only be used to "convert" (downcast or upcast) between comparable types (one has to be a subtype of the other one for it to work)... with a "beautiful" exception of:
declare const b: "b"
const a = "a" as typeof b;
Those shouldn't quite be comparable - two literal types don't form the subtype/supertype relationship and yet this casting is allowed. I think I read that this was a bug that slipped through for enough period of time that it has now been codified as a feature :v
const paulsBoutiqueSales = paulsBoutique as SalesData; | ||
``` | ||
|
||
Again, TypeScript shows us the warning about the lack of common properties. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since u mention common properties... it's also worth mentioning that TypeScript has a concept of "weak types". In an alternative universe this could be fine but in TS it isn't:
declare const obj1: { a?: number };
const obj2: { b?: number } = obj1; // error
When the type has only optional properties then a common property is still required for the assignment to work.
const id = searchParams.get("id")!; | ||
``` | ||
|
||
This forces TypeScript to treat `id` as a string, even though it could be `null` at runtime. It's the equivalent of using `as string`, but is a little more convenient. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This forces TypeScript to treat `id` as a string, even though it could be `null` at runtime. It's the equivalent of using `as string`, but is a little more convenient. | |
This forces TypeScript to treat `id` as a string, even though it could be `null` at runtime. It's the equivalent of using `as string` in this example, but is a little more convenient. |
It's more like an equivalent of as NonNullable<SomeNullableType>
/as NonNullable<typeof foo>
|
||
#### `as any` vs Error Suppression Directives | ||
|
||
When there's a choice with how to suppress an error, I prefer using `as any`. Error suppression directives are too broad - they target the entire line of code. This can lead to accidentally suppressing errors that you didn't mean to: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's also somewhat "easy" to reason about as any
because there are certain rules around it. We know how it propagates through types etc. A suppression might easily result in an error type that is displayed as any
on hover... but it doesn't really have well-defined rules around it and there is no plan to codify them. All bets are off at that point. In fact, one behavior around them changed in the recent 5.5... so it actually broke some libraries/tools relying on the old behavior and there is no intention to bring back the old behavior
|
||
In the first, we construct an object by passing in the keys and values. In the second, we construct an empty object and add the keys and values later. The first pattern is static, the second is dynamic. | ||
|
||
But in TypeScript, the first pattern is much easier to work with. TypeScript can infer the type of `obj` as `{ a: number, b: number }`. But it can't infer the type of `obj2` - it's just an empty object. In fact, you'll get errors when you try to do this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fun fact, TS supports the dynamic variant in JS files :D it doesn't do it in overly safe way though:
const obj = {}
obj.a = 1
if (Math.random()) {
obj.b = 'foo'
}
obj // { a: number; b: string }
|
||
Think of the time you invest in fixing TypeScript errors as an investment in yourself. You're both fixing potential bugs in the future, and levelling up your own understanding. | ||
|
||
## Exercises |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
weirdly there are no exercises 1, 3 and 5 here
|
||
<Exercise title="Exercise 8: Create a Deeply Read-Only Object" filePath="/src/045-annotations-and-assertions/148-satisfies-with-as-const.problem.ts"></Exercise> | ||
|
||
### Solution 2: Provide Additional Info to TypeScript |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm surprised that a solution using a non-null assertion isn't mentioned here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you mentioned auto-types elsewhere but I'm not sure if u mentioned auto array types. I'm not sure if it's the best chapter to dive deeper into them but I thought about them when reading this so i figured out that it might be worth sharing:
let something
if (Math.random()) {
something = 1
} else {
something = 'foo'
}
something // number | string
let someArray = []
if (Math.random()) {
someArray.push(1)
} else {
someArray.push('foo')
}
someArray // (number | string)[]
TS is pretty smart 😅
One extra thing I thought of is this thing:
function test1() {
let foo: string // allowed but even if we don't assign string right away
return foo // it's smart enough to reject it now because it wasn't initialized!
}
function test2() {
let foo: string
if (Math.random()) {
foo = 'a'
} else {
foo = 'b'
}
return foo // it's fine, it knows that it has been initialized!
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, i see that auto/evolving types are mentioned right in the next chapter, cool :)
No description provided.