From 24423d108b1397a19bb7ec18975a2e1cb3c97718 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 30 May 2022 22:03:15 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20[WIP]=20first=20two=20a?= =?UTF-8?q?rticles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docusaurus.config.js | 13 ++ package.json | 1 + src/content/articles/example.mdx | 3 - src/content/articles/for-in-string-types.mdx | 211 ++++++++++++++++++ .../objects-narrowed-before-usage.mdx | 60 +++++ src/content/articles/other.mdx | 3 - src/css/custom.css | 32 ++- yarn.lock | 117 +++++++++- 8 files changed, 422 insertions(+), 18 deletions(-) delete mode 100644 src/content/articles/example.mdx create mode 100644 src/content/articles/for-in-string-types.mdx create mode 100644 src/content/articles/objects-narrowed-before-usage.mdx delete mode 100644 src/content/articles/other.mdx diff --git a/docusaurus.config.js b/docusaurus.config.js index 18e9130..cdba4f1 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -37,6 +37,8 @@ const config = { editUrl: "https://github.com/JoshuaKGoldberg/learning-typescript/tree/main/", path: "src/content/articles", + readingTime: (args) => + Math.ceil(Math.pow(args.defaultReadingTime(args), 1.5) / 5) * 5, routeBasePath: "articles", showReadingTime: true, }, @@ -53,6 +55,17 @@ const config = { }, }), ], + [ + "docusaurus-preset-shiki-twoslash", + { + // Todo: ...is this being respected?! + defaultCompilerOptions: { + lib: ["dom", "es2021"], + target: "esnext", + }, + themes: ["min-light", "nord"], + }, + ], ], themeConfig: diff --git a/package.json b/package.json index a8057a6..43f1f24 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@mdx-js/react": "^1.6.22", "@swc/core": "^1.2.194", "clsx": "^1.1.1", + "docusaurus-preset-shiki-twoslash": "^1.1.37", "mdast-util-from-markdown": "^1.2.0", "prism-react-renderer": "^1.3.1", "react": "^17.0.2", diff --git a/src/content/articles/example.mdx b/src/content/articles/example.mdx deleted file mode 100644 index e3d6c8c..0000000 --- a/src/content/articles/example.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Example Article - -Hooray! diff --git a/src/content/articles/for-in-string-types.mdx b/src/content/articles/for-in-string-types.mdx new file mode 100644 index 0000000..25fb22b --- /dev/null +++ b/src/content/articles/for-in-string-types.mdx @@ -0,0 +1,211 @@ +--- +date: "June 3 2022" +summary: "" +--- + +# Why for...in and Object.keys() Give String Types + +Why does TypeScript report a `string` type instead of `keyof typeof` the object when you use a `for...in` loop or `Object.keys()` on an object? + +One of the most commonly asked questions in TypeScript help forums is why code like the following gives an error under the [`noImplicitAny` compiler option](https://www.typescriptlang.org/tsconfig#noImplicitAny). + +```ts twoslash +// @errors: 7053 +const counts = { + one: 1, + two: 2, +}; + +for (const i in counts) { + // ^? + console.log(counts[i]); +} +``` + +Shouldn't TypeScript know that `i` is type `"one" | "two"`, a.k.a. `keyof typeof counts`? +Why is it telling us that `i` is `string` and can't be used like `counts[i]`? + +The answer is logical -though often inconvenient- and requires understanding a key characteristic of TypeScript's type system. + + + +## Objects May Have More Properties Than They Appear + +TypeScript's type system is _structurally typed_: meaning any value that happens to satisfy a type is allowed to be used as a value of that type. +In other words, when you declare that a value is of a particular object type, you're telling TypeScript any object that just so happens to have those properties is allowed to be considered that type. + +As a result, when you write a `for...in` loop over the keys of an object, you often won't have a guarantee that the object's keys will only be from the type of the object. +Some other area of code might have provided an object with more properties that just so happens to match the structural type. + +Take a look at this completely type safe code snippet that passes an object to a function. +What types would you expect for `i` inside the function's `for` loop? + +```ts twoslash +interface TwoCounts { + one: number; + two: number; +} + +function iterateOverCounts(counts: TwoCounts) { + for (const i in counts) { + // ... + } +} + +const counts = { + one: 1, + two: 2, + three: 3, +}; + +iterateOverCounts(counts); +``` + +`i` at _runtime_ will be `"one"`, `"two"`, then `"three"`. +But there's no reasonable way for TypeScript's type system to know that. +It just knows that `counts` is type `TwoCounts`, which has keys `"one"` and `"two"`. + +Thus, if TypeScript tried to assume that `i` was `keyof typeof counts` then the type system could be insidiously wrong. +For example, your code might assume `i` is only ever `"one"` or `"two`", and use it in logic that would crash with any other value! + +See [Workarounds](#workarounds) below for how you can get around this behavior in `for...in` loops. + +:::tip +Wondering why the first code snippet couldn't narrow `counts` to a more precise type? +See also [Why Objects Aren't Narrowed Before Usage](/articles/objects-narrowed-before-usage). +::: + +### `Object.keys()` Too + +This same design quirk applies to `Object.keys()`: + +```ts twoslash +const counts = { + one: 1, + two: 2, +}; + +Object.keys(counts); +// ^? +``` + +`Object.keys()` and `for...in` loops work almost the same way. +The main difference is that `Object.keys()` only looks at properties on the object itself, while `for...in` loops also look at properties on the object's `prototype` chain. + +See [Workarounds](#workarounds) below for how you can get around this behavior in `Object.keys()` as well. + +## Workarounds + +Before you try to work around the design of the type checker, please pause a moment and reflect. +The situations mentioned earlier in this post are real problems in JavaScript and TypeScript code that have caused countless bugs in software. + +**TypeScript is right to assume `string`s types.** + +### `Object` APIs + +TypeScript's type checking is designed to work with how JavaScript data typically flows and changes through an application. +Needing to wrestle with the type checker is often a byproduct of application code going against safe JavaScript practices -- such as the aforementioned issues with `for...in` loops receiving unexpected key strings. +In many cases, it might be good to listen to TypeScript and consider refactoring your code to use a JavaScript API better suited to the task. + +If all your code needs to do is get all the values in an object, an `Object.values()` API exists that returns those as an array. +You can `for...of` loop over them instead: + +```ts twoslash +// @lib: dom,esnext + +const counts = { + one: 1, + two: 2, +}; + +for (const [key, value] of Object.entries(counts)) { + // ^? +} +``` + +Alternately, if you do need keys, consider using the `Object.entries()` API instead. +It returns a `[string, T][]` where `T` is the type of values in an object: + +```ts twoslash +// @lib: dom,esnext + +const counts = { + one: 1, + two: 2, +}; + +for (const [key, value] of Object.entries(counts)) { + // ^? +} +``` + +Both `Object.entries()` and `Object.values()` allow you to write clean, idiomatic JavaScript without having to grapple with the type checker. + +### `Map` + +Taking an even larger step back, why is the application code looping over an object's keys and values? +Is the object meant to be used as a general key-value store? + +If so: consider using the built-in `Map` data structure instead. +It's made specifically for key-value stores. + +You can `for...of` over a `Map`'s values using `Array.from`: + +```ts twoslash +// @lib: dom,esnext + +const counts = new Map([ + ["one", 1], + ["two", 2], +]); + +for (const [key, value] of Array.from(counts)) { + // ^? +} +``` + +`Map`s have the added benefit over `{}` objects of allowing any object to be a key without stringifying them first (which objects do). + +### Type Assertions + +Still, there are times when a `for...in` loop or `Object.keys()` might likely be mostly safe to use for object keys. +If you are 100% absolutely completely sure that your use case won't crash from some bizarre type mismatch now or in the future, you can always use an `as` assertion to cast the loop variable to the right type. + +One assertion strategy is to use `keyof typeof` to get the keys (`keyof`) of the object's type (`typeof`): + +```ts twoslash +const counts = { + one: 1, + two: 2, +}; + +for (const i in counts) { + console.log(counts[i as keyof typeof counts]); // Ok +} +``` + +Aternately, if the object you're looping over is of a previously declared type, you can use the `keyof` operator on that type to get back a string literal union of its keys: + +```ts twoslash +interface Counts { + one: number; + two: number; +} + +const counts: Counts = { + one: 1, + two: 2, +}; + +for (const i in counts) { + console.log(counts[i as keyof Counts]); // Ok +} +``` + +## Further Reading + +See [this comment from Anders Hejlsberg](https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208) and its surrounding PR for more details to this blog post on why the TypeScript team opted to keep this safer behavior. + +The TypeScript team and community have been discussing a [pending issue for "Exact" types](https://github.com/microsoft/TypeScript/issues/12936). +Exact types wouldn't allow arbitrary extra keys, so `for...in` loops over them would give the more narrow key types. +The issue has been open since 2016, though, so don't get your hopes up 😉. diff --git a/src/content/articles/objects-narrowed-before-usage.mdx b/src/content/articles/objects-narrowed-before-usage.mdx new file mode 100644 index 0000000..51a487f --- /dev/null +++ b/src/content/articles/objects-narrowed-before-usage.mdx @@ -0,0 +1,60 @@ +--- +date: "June 1 2022" +description: "yippee!..." +--- + +# Why Objects Aren't Narrowed Before Usage + +```ts twoslash +const counts = { + one: 1, + two: 2, +}; + +const twoValue = counts.two; +// ^? +``` + +Shouldn't TypeScript know that `counts.two` is specifically the literal type `2` and not the general primitive type `number`? +Can't it tell we haven't changed the object yet. + +It can't, but it won't. +And for very good reason. + + + +## Change Tracking is Impractical + +The problem is that it's difficult -oftentimes impossible- for TypeScript to know whether an object has been modified if anything has happened between its initialization and first usage. +If the object is used in any meaningful application logic, TypeScript won't be able to tell whether it was modified. + +### A Practical Example + +Let's say you want to `console.log` the `counts` object before usage. +Your use your own custom `log` function that just so happens to only call `console.log`? +You'd still want `counts.two` to be `2`, right? + +```ts +function log(data: unknown) { + console.log(data); +} + +const counts = { + one: 1, + two: 2, +}; + +log(counts); + +const twoValue = counts.two; +// ^? +``` + +In theory, the `log` function could use the `readonly` modifier on its `data` parameter to indicate that +But many built-in user type definitions don't properly mark parameters as read-only. +Even `console.log` isn' marked as read-only as of TypeScript 4.7.2. + +## Further Reading + +A legendary [Trade-offs in Control Flow Analysis](https://github.com/microsoft/TypeScript/issues/9998) issue exists in the TypeScript repository. +It describes many more common trade-offs in how the TypeScript type checker analyzes the flow of values. diff --git a/src/content/articles/other.mdx b/src/content/articles/other.mdx deleted file mode 100644 index a411698..0000000 --- a/src/content/articles/other.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Other Example - -Yay! diff --git a/src/css/custom.css b/src/css/custom.css index c9f6ce4..1e4b15b 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -16,17 +16,6 @@ --ifm-background-deemphasized-color: #222024; } -.docusaurus-highlight-code-line { - background-color: rgba(0, 0, 0, 0.1); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -[data-theme="dark"] .docusaurus-highlight-code-line { - background-color: rgba(0, 0, 0, 0.3); -} - /* Header */ .navbar__items { @@ -41,3 +30,24 @@ .navbar__title { margin-left: 0.75rem; } + +/* Code & Twoslash */ + +[data-theme="dark"] .shiki.min-light { + display: none; +} + +[data-theme="light"] .shiki.nord { + display: none; +} + +.docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.1); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +[data-theme="dark"] .docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.3); +} diff --git a/yarn.lock b/yarn.lock index f0b5741..525a0e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2869,6 +2869,29 @@ dependencies: "@types/node" "*" +"@typescript/twoslash@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript/twoslash/-/twoslash-3.1.0.tgz#20b4d4bb58729681ff7f4b187af98757ea8723d4" + integrity sha512-kTwMUQ8xtAZaC4wb2XuLkPqFVBj2dNBueMQ89NWEuw87k2nLBbuafeG5cob/QEr6YduxIdTVUjix0MtC7mPlmg== + dependencies: + "@typescript/vfs" "1.3.5" + debug "^4.1.1" + lz-string "^1.4.4" + +"@typescript/vfs@1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@typescript/vfs/-/vfs-1.3.4.tgz#07f89d2114f6e29255d589395ed7f3b4af8a00c2" + integrity sha512-RbyJiaAGQPIcAGWFa3jAXSuAexU4BFiDRF1g3hy7LmRqfNpYlTQWGXjcrOaVZjJ8YkkpuwG0FcsYvtWQpd9igQ== + dependencies: + debug "^4.1.1" + +"@typescript/vfs@1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@typescript/vfs/-/vfs-1.3.5.tgz#801e3c97b5beca4ff5b8763299ca5fd605b862b8" + integrity sha512-pI8Saqjupf9MfLw7w2+og+fmb0fZS0J6vsKXXrp4/PDXEFvntgzXmChCXC/KefZZS0YGS6AT8e0hGAJcTsdJlg== + dependencies: + debug "^4.1.1" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -4241,6 +4264,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docusaurus-preset-shiki-twoslash@^1.1.37: + version "1.1.37" + resolved "https://registry.yarnpkg.com/docusaurus-preset-shiki-twoslash/-/docusaurus-preset-shiki-twoslash-1.1.37.tgz#46f59c78a2fc619498b87e9d2c029fbd0758419d" + integrity sha512-dXDhVhEL3dgFaG/Oreq2QWxI4L0RcXfTydF8MFXCzXxjA/65c2BSwqJIJDcSZtsZ15YoNQJ2H3467zVsAHgPVg== + dependencies: + copy-text-to-clipboard "^3.0.1" + remark-shiki-twoslash "3.0.9" + typescript ">3" + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -4689,6 +4721,11 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" +fenceparser@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fenceparser/-/fenceparser-1.1.1.tgz#e10a5f12a54884d4f14edb0f7a52a0edc58fa667" + integrity sha512-VdkTsK7GWLT0VWMK5S5WTAPn61wJ98WPFwJiRHumhg4ESNUO/tnkU8bzzzc62o6Uk1SVhuZFLnakmDA4SGV7wA== + file-loader@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" @@ -5683,6 +5720,11 @@ json5@^2.1.2, json5@^2.2.1: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== +jsonc-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" + integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -5896,6 +5938,13 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5903,6 +5952,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -6486,6 +6540,13 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onigasm@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" + integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== + dependencies: + lru-cache "^5.1.1" + open@^8.0.9, open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -7396,7 +7457,7 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== @@ -7519,6 +7580,21 @@ remark-parse@8.0.3: vfile-location "^3.0.0" xtend "^4.0.1" +remark-shiki-twoslash@3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/remark-shiki-twoslash/-/remark-shiki-twoslash-3.0.9.tgz#5c10793733fdf3dd5b2de50f1c220d3ec1f0ff92" + integrity sha512-durv6l16O8F5XY0b0WduewxlOnnc1B83fxWFDOfGX9ehvHw+xcPJbjurDy8m99EpvC5Q/kUG4fbSSK/HwpYxCw== + dependencies: + "@typescript/twoslash" "3.1.0" + "@typescript/vfs" "1.3.4" + fenceparser "^1.1.0" + regenerator-runtime "^0.13.7" + shiki "0.9.11" + shiki-twoslash "3.0.2" + tslib "2.1.0" + typescript ">3" + unist-util-visit "^2.0.0" + remark-squeeze-paragraphs@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead" @@ -7866,6 +7942,25 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" +shiki-twoslash@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/shiki-twoslash/-/shiki-twoslash-3.0.2.tgz#9fa4b17f2982560cc1abfdc8db43b474eae80069" + integrity sha512-2VxUykJ0ETWxageixkfKcMjK5mpMYxJU3uhAoscTZnfX/iq+bWnEpLZnsrcErMJrD85orxABMw5w/GoO4nUiqg== + dependencies: + "@typescript/twoslash" "3.1.0" + "@typescript/vfs" "1.3.4" + shiki "0.9.11" + typescript ">3" + +shiki@0.9.11: + version "0.9.11" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.11.tgz#07d75dab2abb6dc12a01f79a397cb1c391fa22d8" + integrity sha512-tjruNTLFhU0hruCPoJP0y+B9LKOmcqUhTpxn7pcJB3fa+04gFChuEmxmrUfOJ7ZO6Jd+HwMnDHgY3lv3Tqonuw== + dependencies: + jsonc-parser "^3.0.0" + onigasm "^2.2.5" + vscode-textmate "5.2.0" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -8256,6 +8351,11 @@ ts-node@^10.8.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tslib@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tslib@^2.0.3, tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -8291,6 +8391,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript@>3: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== + typescript@^4.6.3: version "4.6.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c" @@ -8570,6 +8675,11 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vscode-textmate@5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" + integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== + wait-on@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" @@ -8842,6 +8952,11 @@ xtend@^4.0.0, xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" From 51986d2dbeb869d762938136ebdaaa5fc113f807 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Wed, 3 Aug 2022 11:07:50 -0400 Subject: [PATCH 2/2] WIP --- docusaurus.config.js | 4 +- package.json | 2 +- src/content/articles/for-in-string-types.mdx | 32 ++-- .../named-properties-and-index-signatures.mdx | 148 ++++++++++++++++++ .../objects-narrowed-before-usage.mdx | 1 + src/css/custom.css | 10 ++ yarn.lock | 78 ++++----- 7 files changed, 218 insertions(+), 57 deletions(-) create mode 100644 src/content/articles/named-properties-and-index-signatures.mdx diff --git a/docusaurus.config.js b/docusaurus.config.js index 94d31ce..1956f90 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -24,6 +24,9 @@ const config = { .readFileSync(path.join(articlesBaseDir, file)) .toString(); + if (!/meta: (.+)/.exec(contents)) { + console.log({ file, contents }); + } return { date: /date: "(.+)"/.exec(contents)[1], description: /description: "(.+)"/.exec(contents)[1], @@ -136,7 +139,6 @@ const config = { [ "docusaurus-preset-shiki-twoslash", { - // Todo: ...is this being respected?! defaultCompilerOptions: { lib: ["dom", "es2021"], target: "esnext", diff --git a/package.json b/package.json index 8f8afca..780b18a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@mdx-js/react": "^1.6.22", "@swc/core": "^1.2.194", "clsx": "^1.1.1", - "docusaurus-preset-shiki-twoslash": "^1.1.37", + "docusaurus-preset-shiki-twoslash": "^1.1.38", "fs-readdir-recursive": "^1.1.0", "konamimojisplosion": "^0.5.1", "mdast-util-from-markdown": "^1.2.0", diff --git a/src/content/articles/for-in-string-types.mdx b/src/content/articles/for-in-string-types.mdx index 25fb22b..7195f9f 100644 --- a/src/content/articles/for-in-string-types.mdx +++ b/src/content/articles/for-in-string-types.mdx @@ -1,6 +1,7 @@ --- date: "June 3 2022" -summary: "" +description: "Exploring how TypeScript infers types for iterators in for...in loops" +meta: for loops, maps, objects, sets, strings --- # Why for...in and Object.keys() Give String Types @@ -11,6 +12,7 @@ One of the most commonly asked questions in TypeScript help forums is why code l ```ts twoslash // @errors: 7053 +// @lib: dom,es2021 const counts = { one: 1, two: 2, @@ -41,6 +43,7 @@ Take a look at this completely type safe code snippet that passes an object to a What types would you expect for `i` inside the function's `for` loop? ```ts twoslash +// @lib: dom,es2021 interface TwoCounts { one: number; two: number; @@ -48,7 +51,7 @@ interface TwoCounts { function iterateOverCounts(counts: TwoCounts) { for (const i in counts) { - // ... + console.log(i); } } @@ -80,6 +83,7 @@ See also [Why Objects Aren't Narrowed Before Usage](/articles/objects-narrowed-b This same design quirk applies to `Object.keys()`: ```ts twoslash +// @lib: dom,es2021 const counts = { one: 1, two: 2, @@ -99,7 +103,7 @@ See [Workarounds](#workarounds) below for how you can get around this behavior i Before you try to work around the design of the type checker, please pause a moment and reflect. The situations mentioned earlier in this post are real problems in JavaScript and TypeScript code that have caused countless bugs in software. -**TypeScript is right to assume `string`s types.** +**TypeScript is right to assume `string` types.** ### `Object` APIs @@ -118,8 +122,8 @@ const counts = { two: 2, }; -for (const [key, value] of Object.entries(counts)) { - // ^? +for (const value of Object.values(counts)) { + // ^? } ``` @@ -135,7 +139,10 @@ const counts = { }; for (const [key, value] of Object.entries(counts)) { - // ^? + key; + // ^? + value; + // ^? } ``` @@ -160,7 +167,10 @@ const counts = new Map([ ]); for (const [key, value] of Array.from(counts)) { - // ^? + key; + // ^? + value; + // ^? } ``` @@ -174,6 +184,7 @@ If you are 100% absolutely completely sure that your use case won't crash from s One assertion strategy is to use `keyof typeof` to get the keys (`keyof`) of the object's type (`typeof`): ```ts twoslash +// @lib: dom,es2021 const counts = { one: 1, two: 2, @@ -184,9 +195,10 @@ for (const i in counts) { } ``` -Aternately, if the object you're looping over is of a previously declared type, you can use the `keyof` operator on that type to get back a string literal union of its keys: +Alternately, if the object you're looping over is of a previously declared type, you can use the `keyof` operator on that type to get back a string literal union of its keys: ```ts twoslash +// @lib: dom,es2021 interface Counts { one: number; two: number; @@ -198,7 +210,9 @@ const counts: Counts = { }; for (const i in counts) { - console.log(counts[i as keyof Counts]); // Ok + const count = counts[i as keyof Counts]; // Ok + // ^? + console.log(count); } ``` diff --git a/src/content/articles/named-properties-and-index-signatures.mdx b/src/content/articles/named-properties-and-index-signatures.mdx new file mode 100644 index 0000000..5803463 --- /dev/null +++ b/src/content/articles/named-properties-and-index-signatures.mdx @@ -0,0 +1,148 @@ +--- +date: "August 07 2022" +description: "Why named properties must be assignable to index signatures in TypeScript object types." +meta: assignability, index signatures, objects, properties +--- + +# Named Properties and Index Signatures in TypeScript + +[Index signatures](https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures) are a TypeScript feature that allow object types to declare that any arbitrary property on a type will have a certain property. +They're useful when you have an object meant to allow any arbitrary property of any name. + +For example, this object shape allows any property of on a value of type `FruitCounts` to have a `number` value: + +```ts twoslash +interface FruitCounts { + [i: string]: number; +} + +const counts: FruitCounts = {}; + +counts.apple = 1; +counts.banana = 2; +``` + +One rarely-used feature of index signatures is that you can mix and match them with named properties: + +```ts twoslash +interface MoreFruitCounts { + [i: string]: number; + + myLeastFavoriteFruit: 0; +} + +const counts: MoreFruitCounts = { + myLeastFavoriteFruit: 0, +}; + +counts.apple; +// ^? + +counts.myLeastFavoriteFruit; +// ^? +``` + +But, there is one restriction: the types of those properties must be assignable to the index signature's type: + +```ts twoslash +// @errors: 2411 +interface FruitCountsWithUnknown { + [i: string]: number; + + unknownFruit: string; +} +``` + +Why is that? +In a word: _assignability_. + + + +## Property Lookups and Assignability + +When an object's type contains an index signature and a property is retrieved from the type, TypeScript needs to be able to assume that the retrieved property is that index signature's type. + +However, if some properties of the object aren't assignable to the index signature's type, then that assumption will be wrong. + +Suppose an interface was allowed to have a property with a mismatched type, and code later retrieved a property from that interface under a dynamic property name -- as with `counts[fruit]` here: + +```ts twoslash +interface FruitCountsWithUnknown { + [i: string]: number; + + // @ts-expect-error + unknownFruit: string; +} + +// @ts-expect-error +const counts: FruitCountsWithUnknown = { + apple: 0, + unknownFruit: "Gotcha!", +}; + +function retrieveFruitCount(fruit: string) { + return counts[fruit]; +} + +const appleCount = retrieveFruitCount("apple"); +// ^? + +const unknownFruitCount = retrieveFruitCount("unknownFruit"); +// ^? +``` + +TypeScript would have no way of knowing when the retrieved property might be the named property's type instead of the index signature's type. + +## Numeric Index Signature Assignability + +JavaScript objects coerce property names to strings. +When you look up a property under a name that's not a string it'll be retrieved under the equivalent string key: + +```ts twoslash +const idCounts = { + "123": 0, +} as const; + +const countUnderNumber = idCounts[123]; +// ^? + +const countUnderString = idCounts["123"]; +// ^? +``` + +As a result, TypeScript allows index signatures over `number`s in addition to `string`s. +But they also have the same restriction as named properties: if a `string` index signature is present, the `number` index signature's types must be assignable to it. + +```ts twoslash +// @errors: 2413 +interface FruitOrIdCounts { + [i: string]: number; + + [i: number]: string; +} +``` + +You can think of both `number` index signatures and named properties to be considered subsets of `string` index signature properties. +Both may have their own, more specific types -- as long as those types are assignable to the catch-all `string` index signature's type. + +```ts twoslash +interface PuttingThemAllTogether { + [i: string]: number; + + myLeastFavoriteFruit: 0; + + [i: number]: 0 | 1 | 2 | 3; +} +``` + +:::note Food for Thought +Still having trouble getting your index signatures to work in TypeScript? +Consider reading [Why for...in and Object.keys() Give String Types](./for-in-string-types): you might be better off using a `Map` or `Set` +::: + +--- + +Index signatures are covered in _Learning TypeScript_'s Chapter 7: Interfaces. + +Got your own TypeScript questions? +Tweet [@LearningTSbook](https://twitter.com/LearningTSBook) and the answer might become an article too! diff --git a/src/content/articles/objects-narrowed-before-usage.mdx b/src/content/articles/objects-narrowed-before-usage.mdx index 51a487f..bd6bddf 100644 --- a/src/content/articles/objects-narrowed-before-usage.mdx +++ b/src/content/articles/objects-narrowed-before-usage.mdx @@ -1,6 +1,7 @@ --- date: "June 1 2022" description: "yippee!..." +meta: objects, narrowing, variables --- # Why Objects Aren't Narrowed Before Usage diff --git a/src/css/custom.css b/src/css/custom.css index 2075996..3ed2cd6 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -74,6 +74,16 @@ display: none; } +/* Override for inaccessible (barely visible) light mode comments */ +.shiki.min-light span[style="color: rgb(194, 195, 197);"] { + color: #576 !important; +} + +/* Override for inaccessible (barely visible) dark mode comments */ +.shiki.nord span[style="color: rgb(97, 110, 136);"] { + color: #8a9 !important; +} + /* Bodies */ article .markdown a { diff --git a/yarn.lock b/yarn.lock index 4ef04c1..6213f6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3474,13 +3474,13 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" -docusaurus-preset-shiki-twoslash@^1.1.37: - version "1.1.37" - resolved "https://registry.yarnpkg.com/docusaurus-preset-shiki-twoslash/-/docusaurus-preset-shiki-twoslash-1.1.37.tgz#46f59c78a2fc619498b87e9d2c029fbd0758419d" - integrity sha512-dXDhVhEL3dgFaG/Oreq2QWxI4L0RcXfTydF8MFXCzXxjA/65c2BSwqJIJDcSZtsZ15YoNQJ2H3467zVsAHgPVg== +docusaurus-preset-shiki-twoslash@^1.1.38: + version "1.1.38" + resolved "https://registry.yarnpkg.com/docusaurus-preset-shiki-twoslash/-/docusaurus-preset-shiki-twoslash-1.1.38.tgz#297ddff7b5b84bc6bc9ebf32a3ee1ff17ae0e035" + integrity sha512-v8bp6Q6gUNLuWmf4x19Tk2VvjEc8Luuy6o7fpTg7fTFUwo7ZBVWPTrxJe/aN4OmjkVBmuDfBc/muUGEAJTVm2g== dependencies: copy-text-to-clipboard "^3.0.1" - remark-shiki-twoslash "3.0.9" + remark-shiki-twoslash "3.1.0" typescript ">3" dom-converter@^0.2.0: @@ -4936,9 +4936,9 @@ json5@^2.1.2, json5@^2.2.1: integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.1.0.tgz#73b8f0e5c940b83d03476bc2e51a20ef0932615d" + integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg== jsonfile@^6.0.1: version "6.1.0" @@ -5166,13 +5166,6 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5753,13 +5746,6 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onigasm@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/onigasm/-/onigasm-2.2.5.tgz#cc4d2a79a0fa0b64caec1f4c7ea367585a676892" - integrity sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA== - dependencies: - lru-cache "^5.1.1" - open@^8.0.9, open@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" @@ -6779,17 +6765,17 @@ remark-parse@8.0.3: vfile-location "^3.0.0" xtend "^4.0.1" -remark-shiki-twoslash@3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/remark-shiki-twoslash/-/remark-shiki-twoslash-3.0.9.tgz#5c10793733fdf3dd5b2de50f1c220d3ec1f0ff92" - integrity sha512-durv6l16O8F5XY0b0WduewxlOnnc1B83fxWFDOfGX9ehvHw+xcPJbjurDy8m99EpvC5Q/kUG4fbSSK/HwpYxCw== +remark-shiki-twoslash@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/remark-shiki-twoslash/-/remark-shiki-twoslash-3.1.0.tgz#b5e58eec9f97458ade1df73cb96566d031cc75bd" + integrity sha512-6LqSqVtHQR4S0DKfdQ2/ePn9loTKUtpyopYvwk8johjDTeUW5MkaLQuZHlWNkkST/4aMbz6aTkstIcwfwcHpXg== dependencies: "@typescript/twoslash" "3.1.0" "@typescript/vfs" "1.3.4" fenceparser "^1.1.0" regenerator-runtime "^0.13.7" - shiki "0.9.11" - shiki-twoslash "3.0.2" + shiki "0.10.1" + shiki-twoslash "3.1.0" tslib "2.1.0" typescript ">3" unist-util-visit "^2.0.0" @@ -7141,23 +7127,23 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -shiki-twoslash@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/shiki-twoslash/-/shiki-twoslash-3.0.2.tgz#9fa4b17f2982560cc1abfdc8db43b474eae80069" - integrity sha512-2VxUykJ0ETWxageixkfKcMjK5mpMYxJU3uhAoscTZnfX/iq+bWnEpLZnsrcErMJrD85orxABMw5w/GoO4nUiqg== +shiki-twoslash@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/shiki-twoslash/-/shiki-twoslash-3.1.0.tgz#818aaa15e83e0f34325dd56a991f7023f7e8e6db" + integrity sha512-uDqrTutOIZzyHbo103GsK7Vvc10saK1XCCivnOQ1NHJzgp3FBilEpftGeVzVSMOJs+JyhI7whkvhXV7kXQ5zCg== dependencies: "@typescript/twoslash" "3.1.0" "@typescript/vfs" "1.3.4" - shiki "0.9.11" + shiki "0.10.1" typescript ">3" -shiki@0.9.11: - version "0.9.11" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.9.11.tgz#07d75dab2abb6dc12a01f79a397cb1c391fa22d8" - integrity sha512-tjruNTLFhU0hruCPoJP0y+B9LKOmcqUhTpxn7pcJB3fa+04gFChuEmxmrUfOJ7ZO6Jd+HwMnDHgY3lv3Tqonuw== +shiki@0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.10.1.tgz#6f9a16205a823b56c072d0f1a0bcd0f2646bef14" + integrity sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng== dependencies: jsonc-parser "^3.0.0" - onigasm "^2.2.5" + vscode-oniguruma "^1.6.1" vscode-textmate "5.2.0" signal-exit@^3.0.2, signal-exit@^3.0.3: @@ -7567,9 +7553,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@>3: - version "4.7.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" - integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typescript@^4.6.3: version "4.6.3" @@ -7845,6 +7831,11 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +vscode-oniguruma@^1.6.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" + integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== + vscode-textmate@5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" @@ -8122,11 +8113,6 @@ xtend@^4.0.0, xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"