Resolve type problems #1

Merged
ctmiller merged 1 commits from accessing-elements-with-ts into main 5 months ago
cmdln commented 5 months ago
Collaborator
  • Re-factor to be more idiomatic
  • Introduce assertion functions and narrowing
- Re-factor to be more idiomatic - Introduce assertion functions and narrowing
cmdln reviewed 5 months ago
cmdln reviewed 5 months ago
@ -15,3 +13,1 @@
day: string;
mealdesc: string;
}
type MenuItem = {
cmdln commented 5 months ago
Poster
Collaborator

type is more flexible and preferred unless you need a class for the few additional things it provides.

type can be used to compose new types out of other types.

type OrderedMenuItem = MenuItem & { order: number };

And even more interesting examples that you cannot replicated easily with classes alone.

// Partial is a mapped type, it affectss the type passed to it as a generic argument, making all the fields optional
type OptionalMenuItem = Partial<MenuItem>;
`type` is more flexible and preferred unless you need a class for the few additional things it provides. `type` can be used to compose new types out of other types. ```typescript type OrderedMenuItem = MenuItem & { order: number }; ``` And even more interesting examples that you cannot replicated easily with classes alone. ```typescript // Partial is a mapped type, it affectss the type passed to it as a generic argument, making all the fields optional type OptionalMenuItem = Partial<MenuItem>; ```
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -17,1 +13,3 @@
}
type MenuItem = {
day: string;
mealDesc: string;
cmdln commented 5 months ago
Poster
Collaborator

Using two spaces for indentation is currently very common, probably owes to nested callback hell before Promise then async/await.

mealdesc to mealDesc because camelCase is currently more idiomatic.

Using two spaces for indentation is currently very common, probably owes to nested callback hell before `Promise` then `async/await`. `mealdesc` to `mealDesc` because camelCase is currently more idiomatic.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -21,2 +18,2 @@
MenuItems: Array<MenuItem> = [];
}
type Menu = {
menuItems: MenuItem[];
cmdln commented 5 months ago
Poster
Collaborator

Functionally equivalent but Type[] seems to be far more common in my reading.

InitialCapCamelCase is idiomatically reserved for classes and types.

Functionally equivalent but `Type[]` seems to be far more common in my reading. InitialCapCamelCase is idiomatically reserved for classes and types.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -23,2 +20,3 @@
};
let NewMenu = new Menu();
const newMenu: Menu = {
cmdln commented 5 months ago
Poster
Collaborator

Favoring const where you can use it is more idiomatic. const only prevents re-assigning the name. You can still modify an object the variable refers to.

With type you use a JavaScript object literal to initialize. The Typescript compiler will help you make sure you initialize all the required fields and don't add invalid fields or incorrect values if it can infer the type or you give it a type annotation.

Thought seems to be mixed on type annotations since Typescript can infer in most cases. Some still use annotations everywhere despite inference. Others maybe push too far to avoid them. My rule is to start without them and add them when tsc disagrees with me. Whichever of us is right, judicious hints help us figure out why we disagree. Glob help me, I am anthropomorphizing Typescript

Favoring `const` where you can use it is more idiomatic. `const` only prevents re-assigning the name. You can still modify an object the variable refers to. With `type` you use a JavaScript object literal to initialize. The Typescript compiler will help you make sure you initialize all the required fields and don't add invalid fields or incorrect values if it can infer the type or you give it a type annotation. Thought seems to be mixed on type annotations since Typescript can infer in most cases. Some still use annotations everywhere despite inference. Others maybe push too far to avoid them. My rule is to start without them and add them when `tsc` disagrees with me. Whichever of us is right, judicious hints help us figure out why we disagree. Glob help me, I am anthropomorphizing Typescript
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -32,1 +26,3 @@
NewMenu.MenuItems.push(newMenuItem);
function addMenuItem() {
const { value: day } = getElementById("day", assertIsSelect);
let { value: mealDesc } = getElementById("menuDesc", assertIsInput);
cmdln commented 5 months ago
Poster
Collaborator

Sorry, I missed a let, this could be a const.

Sorry, I missed a `let`, this could be a `const`.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -31,2 +26,2 @@
newMenuItem.mealdesc = txtMenuDesc.value;
NewMenu.MenuItems.push(newMenuItem);
function addMenuItem() {
const { value: day } = getElementById("day", assertIsSelect);
cmdln commented 5 months ago
Poster
Collaborator

Destructuring like this is fairly idiomatic. I also find it pretty erognomic when your types get more complex. Also, you can alias as part of this expression to avoid collisions.

Destructuring like this is fairly idiomatic. I also find it pretty erognomic when your types get more complex. Also, you can alias as part of this expression to avoid collisions.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -33,0 +27,4 @@
const { value: day } = getElementById("day", assertIsSelect);
let { value: mealDesc } = getElementById("menuDesc", assertIsInput);
return {
day,
cmdln commented 5 months ago
Poster
Collaborator

I think the object literal short hand is idiomatic but I may be in the minority. The aliasing in the de-structuring assignments meant we could write the initializer this way. The field names must also happen to match variables in scope for this to work. Very similar to template literal strings, see below.

I think the object literal short hand is idiomatic but I may be in the minority. The aliasing in the de-structuring assignments meant we could write the initializer this way. The field names must also happen to match variables in scope for this to work. Very similar to template literal strings, see below.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -49,2 +35,2 @@
document.body.appendChild(title);
document.body.appendChild(heading);
function renderMenu() {
const msg = newMenu.menuItems.reduce((output, value) => {
cmdln commented 5 months ago
Poster
Collaborator

This is my style. I am leaning more and more into functional programmer. It all starts with filter/map/reduce.

I started with a map to construct the string for each menu item because I thought you were going to assemble an array of strings. map transforms the contents of the data type one to one. You can map far more than an array (and reduce, etc.) but a topic for another day.

I switched to reduce whose purpose is to "reduce" the contents of a data type into a single value. You can reduce types other than arrays but the use cases are rare. reduce usually shows up when you want to change a collection (can include the entries from an object, see Object.prototype.entries.)

You give reduce a lambda whose first argument is the accumulator, or what will become the final product of reduce but for each call of the lambda it will be the accumulated work so far. You must also return this first argument, or another value of the same type, for reduce to work. The second argument is the value from the data type you are reducing, so for an array, each element.

This is my style. I am leaning more and more into functional programmer. It all starts with `filter`/`map`/`reduce`. I started with a `map` to construct the string for each menu item because I thought you were going to assemble an array of strings. `map` transforms the contents of the data type one to one. You can `map` far more than an array (and `reduce`, etc.) but a topic for another day. I switched to `reduce` whose purpose is to "reduce" the contents of a data type into a single value. You *can* reduce types other than arrays but the use cases are rare. `reduce` usually shows up when you want to change a collection (can include the entries from an object, see `Object.prototype.entries`.) You give reduce a lambda whose first argument is the accumulator, or what will become the final product of `reduce` but for each call of the lambda it will be the accumulated work so far. You must also return this first argument, or another value of the same type, for reduce to work. The second argument is the value from the data type you are reducing, so for an array, each element.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -50,1 +35,3 @@
document.body.appendChild(heading);
function renderMenu() {
const msg = newMenu.menuItems.reduce((output, value) => {
return `${output}${value.day}:${value.mealDesc}`;
cmdln commented 5 months ago
Poster
Collaborator

I initially wanted to hate template literal strings but honestly, they are so damn convenient. I rarely preform string operations any other way. This may also be some Rust bias since that comes with powerful string formatting macros that happen to look like template literal strings.

Template literal strings are another deep topic since you can put anything inside the ${}, like function calls or even ternary expressions.

console.log(`${maybeNull === null ? "Object was null!" : maybeNull.someStringField}`);
I initially wanted to hate template literal strings but honestly, they are so damn convenient. I rarely preform string operations any other way. This may also be some Rust bias since that comes with powerful string formatting macros that *happen* to look like template literal strings. Template literal strings are another deep topic since you can put anything inside the `${}`, like function calls or even ternary expressions. ```typescript console.log(`${maybeNull === null ? "Object was null!" : maybeNull.someStringField}`); ```
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -51,2 +47,4 @@
document.body.appendChild(heading);
}
function getElementById<T extends HTMLElement>(
cmdln commented 5 months ago
Poster
Collaborator

I went a little deeper here but we can take time to unpack more.

Typescript not only has generics, we can add constraints. So here, T has to be HTMLElement or assignable to it. I introduced it to capture the type of form element you want to work with.

I went a little deeper here but we can take time to unpack more. Typescript not only has generics, we can add constraints. So here, `T` has to be `HTMLElement` or assignable to it. I introduced it to capture the type of form element you want to work with.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -52,1 +49,4 @@
function getElementById<T extends HTMLElement>(
id: string,
assertion: (e: HTMLElement | null) => asserts e is T
cmdln commented 5 months ago
Poster
Collaborator

Functions are first class in JS and TS and Typescript includes function typing that helps with this. The type of the assertion argument describes what is called an assertion function. Type guards and assertion functions are the answer to the most common question of, "How do I safely access a more specific type?"

The return type of an assertion function is Typescript specific. It is a boolean expression but this syntax does what it says: either e is the specific kind of T we want, or throw an exception.

A type guard, for completeness, only returns a boolean which you can use to help "narrow" the type in question. "narrowing" we touched on, it looks like casting but has more to do with flow control analysis than concrete types. IMO that actually makes it more powerful.

typeGuard: (e: HTMLElement | null): e is T

The input arguments are whatever you need, you can pass in other things. What makes them assertion functions and type guards is that special return type syntax.

Functions are first class in JS and TS and Typescript includes function typing that helps with this. The type of the `assertion` argument describes what is called an assertion function. Type guards and assertion functions are the answer to the most common question of, "How do I safely access a more specific type?" The return type of an assertion function is Typescript specific. It is a boolean expression but this syntax does what it says: either `e` is the specific kind of `T` we want, or throw an exception. A type guard, for completeness, only returns a boolean which you can use to help "narrow" the type in question. "narrowing" we touched on, it looks like casting but has more to do with flow control analysis than concrete types. IMO that actually makes it more powerful. ```typescript typeGuard: (e: HTMLElement | null): e is T ``` The input arguments are whatever you need, you can pass in other things. What makes them assertion functions and type guards is that special return type syntax.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -53,0 +51,4 @@
id: string,
assertion: (e: HTMLElement | null) => asserts e is T
): T {
const e = document.getElementById(id);
cmdln commented 5 months ago
Poster
Collaborator

If you look at the type of e, hover over it in Code, here it is HTMLElement...

If you look at the type of e, hover over it in Code, here it is HTMLElement...
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -53,0 +53,4 @@
): T {
const e = document.getElementById(id);
assertion(e);
return e;
cmdln commented 5 months ago
Poster
Collaborator

...but after our assertion function, it is now the expected type. That is narrowing in action. If you think of an exception as one branch of a conditional, the non-error case is another where we can safely say our type must be the one the assertion function checked.

I could have used a type guard but then we have to do something in the false case which likely would have been to throw an exception anyway.

...but after our assertion function, it is now the expected type. That is narrowing in action. If you think of an exception as one branch of a conditional, the non-error case is another where we can safely say our type must be the one the assertion function checked. I could have used a type guard but then we have to do something in the false case which likely would have been to throw an exception anyway.
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -53,1 +56,4 @@
return e;
}
function assertIsSelect(e: HTMLElement | null): asserts e is HTMLSelectElement {
cmdln commented 5 months ago
Poster
Collaborator

document.getElementById returns HTMLElement | null. The | says the type is a union of HTMLElement or null. Our type has to be one of those but that is frustratingly unhelpful. So our assertion functions take in this type (or we could use the blankent unknown but that is overkill here and loses some expression power.)

`document.getElementById` returns `HTMLElement | null`. The `|` says the type is a union of `HTMLElement` or `null`. Our type has to be one of those but that is frustratingly unhelpful. So our assertion functions take in this type (or we could use the blankent `unknown` but that is overkill here and loses some expression power.)
ctmiller marked this conversation as resolved
cmdln reviewed 5 months ago
@ -54,2 +68,3 @@
function assertIsInput(e: HTMLElement | null): asserts e is HTMLInputElement {
if (e === null || e.tagName.toLowerCase() !== "input") {
cmdln commented 5 months ago
Poster
Collaborator

The body of the assertion function or type guard then is whatever logic you need or want to ensure the type is what you need it to be. Usually a lot of repetitive type and value checking. I have written some tidy utilities we can go over when you are ready to create guards and assertions on the fly, declaratively. ;)

You could also never throw or awlays return true to silence tsc but there is another, more idiomatic syntax for the "no, tsc, trust me it absolutely has to be this type" that also isn't strictly casting. We can go over that later but you may never need it. More helpful to understand it if you see it.

The body of the assertion function or type guard then is whatever logic you need or want to ensure the type is what you need it to be. Usually a lot of repetitive type and value checking. I have written some tidy utilities we can go over when you are ready to create guards and assertions on the fly, declaratively. ;) You could also never throw or awlays return true to silence `tsc` but there is another, more idiomatic syntax for the "no, tsc, trust me it absolutely has to be this type" that also isn't strictly casting. We can go over that later but you may never need it. More helpful to understand it if you see it.
ctmiller marked this conversation as resolved
cmdln commented 5 months ago
Poster
Collaborator

I also added some minimal project config since I don't use code.

I also added some minimal project config since I don't use code.
ctmiller merged commit c95ca6ba81 into main 5 months ago
The pull request has been merged as c95ca6ba81.
You can also view command line instructions.

Step 1:

From your project repository, check out a new branch and test the changes.
git checkout -b accessing-elements-with-ts main
git pull origin accessing-elements-with-ts

Step 2:

Merge the changes and update on Forgejo.
git checkout main
git merge --no-ff accessing-elements-with-ts
git push origin main
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: ctmiller/menuplanner#1
Loading…
There is no content yet.