Skip to content

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

Open
wants to merge 1 commit into
base: initial
Choose a base branch
from
Open

Chapter 11 #11

wants to merge 1 commit into from

Conversation

Andarist
Copy link
Owner

No description provided.

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.
Copy link
Owner Author

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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 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
Copy link
Owner Author

@Andarist Andarist Jul 21, 2024

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

Suggested change
### 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.
Copy link
Owner Author

@Andarist Andarist Jul 21, 2024

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.
Copy link
Owner Author

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

Copy link
Owner Author

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.
Copy link
Owner Author

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.
Copy link
Owner Author

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.
Copy link
Owner Author

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.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:
Copy link
Owner Author

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.
Copy link
Owner Author

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
Copy link
Owner Author

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
Copy link
Owner Author

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

Copy link
Owner Author

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!
}

Copy link
Owner Author

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 :)

@Andarist Andarist marked this pull request as ready for review July 21, 2024 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant