Resolve type problems
#1
Merged
ctmiller
merged 1 commits from accessing-elements-with-ts
into main
5 months ago
Loading…
Reference in New Issue
There is no content yet.
Delete Branch 'accessing-elements-with-ts'
Deleting a branch is permanent. It CANNOT be undone. Continue?
@ -15,3 +13,1 @@
day: string;
mealdesc: string;
}
type 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.And even more interesting examples that you cannot replicated easily with classes alone.
@ -17,1 +13,3 @@
}
type MenuItem = {
day: string;
mealDesc: string;
Using two spaces for indentation is currently very common, probably owes to nested callback hell before
Promise
thenasync/await
.mealdesc
tomealDesc
because camelCase is currently more idiomatic.@ -21,2 +18,2 @@
MenuItems: Array<MenuItem> = [];
}
type Menu = {
menuItems: MenuItem[];
Functionally equivalent but
Type[]
seems to be far more common in my reading.InitialCapCamelCase is idiomatically reserved for classes and types.
@ -23,2 +20,3 @@
};
let NewMenu = new Menu();
const newMenu: Menu = {
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@ -32,1 +26,3 @@
NewMenu.MenuItems.push(newMenuItem);
function addMenuItem() {
const { value: day } = getElementById("day", assertIsSelect);
let { value: mealDesc } = getElementById("menuDesc", assertIsInput);
Sorry, I missed a
let
, this could be aconst
.@ -31,2 +26,2 @@
newMenuItem.mealdesc = txtMenuDesc.value;
NewMenu.MenuItems.push(newMenuItem);
function addMenuItem() {
const { value: day } = getElementById("day", assertIsSelect);
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.
@ -33,0 +27,4 @@
const { value: day } = getElementById("day", assertIsSelect);
let { value: mealDesc } = getElementById("menuDesc", assertIsInput);
return {
day,
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.
@ -49,2 +35,2 @@
document.body.appendChild(title);
document.body.appendChild(heading);
function renderMenu() {
const msg = newMenu.menuItems.reduce((output, value) => {
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 canmap
far more than an array (andreduce
, 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, seeObject.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.@ -50,1 +35,3 @@
document.body.appendChild(heading);
function renderMenu() {
const msg = newMenu.menuItems.reduce((output, value) => {
return `${output}${value.day}:${value.mealDesc}`;
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.@ -51,2 +47,4 @@
document.body.appendChild(heading);
}
function getElementById<T extends HTMLElement>(
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 beHTMLElement
or assignable to it. I introduced it to capture the type of form element you want to work with.@ -52,1 +49,4 @@
function getElementById<T extends HTMLElement>(
id: string,
assertion: (e: HTMLElement | null) => asserts e is T
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 ofT
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.
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.
@ -53,0 +51,4 @@
id: string,
assertion: (e: HTMLElement | null) => asserts e is T
): T {
const e = document.getElementById(id);
If you look at the type of e, hover over it in Code, here it is HTMLElement...
@ -53,0 +53,4 @@
): T {
const e = document.getElementById(id);
assertion(e);
return e;
...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.
@ -53,1 +56,4 @@
return e;
}
function assertIsSelect(e: HTMLElement | null): asserts e is HTMLSelectElement {
document.getElementById
returnsHTMLElement | null
. The|
says the type is a union ofHTMLElement
ornull
. 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 blankentunknown
but that is overkill here and loses some expression power.)@ -54,2 +68,3 @@
function assertIsInput(e: HTMLElement | null): asserts e is HTMLInputElement {
if (e === null || e.tagName.toLowerCase() !== "input") {
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.I also added some minimal project config since I don't use code.
c95ca6ba81
into main 5 months agoc95ca6ba81
.Step 1:
From your project repository, check out a new branch and test the changes.Step 2:
Merge the changes and update on Forgejo.