From f2baf666d07eccff8457edc0f9c8c7d639ac0c6a Mon Sep 17 00:00:00 2001 From: Christian van der Loo Date: Sun, 15 Mar 2026 15:11:18 -0400 Subject: [PATCH] format --- README.md | 1 + astro.config.ts | 7 +- bun.lock | 13 +- package.json | 5 +- src/base.css | 1 - src/content/blog/borrowing-network-effects.md | 2 +- src/content/blog/owning-your-data.md | 6 +- src/content/blog/ts-mapped-types.md | 131 +++++++++--------- src/pages/client-metadata.json.ts | 2 +- src/pages/feed.xml.ts | 14 +- src/pages/index.astro | 56 ++++---- tsconfig.json | 17 +-- wrangler.jsonc | 22 +-- 13 files changed, 136 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index dc69f93..3b17042 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # chrisvanderloo.com ## Sync Records + For each record, use `goat record create` or `goat record update` to sync it with the PDS. diff --git a/astro.config.ts b/astro.config.ts index 37177b4..4898556 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -6,10 +6,7 @@ import cloudflare from "@astrojs/cloudflare"; export default defineConfig({ site: "https://chrisvanderloo.com/", - integrations: [ - icon(), - sitemap(), - ], + integrations: [icon(), sitemap()], vite: { plugins: [tailwindcss() as any], }, @@ -17,7 +14,7 @@ export default defineConfig({ shikiConfig: { themes: { light: "catppuccin-latte", - dark: "catppuccin-mocha" + dark: "catppuccin-mocha", }, }, }, diff --git a/bun.lock b/bun.lock index 0bc8d32..373ba7c 100644 --- a/bun.lock +++ b/bun.lock @@ -24,6 +24,7 @@ "@tailwindcss/vite": "^4.2.1", "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.16.1", + "prettier-plugin-astro": "^0.14.1", }, }, }, @@ -36,7 +37,7 @@ "@astrojs/cloudflare": ["@astrojs/cloudflare@13.1.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.8.0", "@astrojs/underscore-redirects": "1.0.1", "@cloudflare/vite-plugin": "^1.25.6", "piccolore": "^0.1.3", "tinyglobby": "^0.2.15", "vite": "^7.3.1" }, "peerDependencies": { "astro": "^6.0.0-alpha.0", "wrangler": "^4.61.1" } }, "sha512-gnHD3XhBKRoQJc5OAp1g2blGKoxLW8X4VEmN30K/M3cNkyACAG1j8UdLHNb4wAjTwFTIJ7O2KmUNeg8GUJKjMA=="], - "@astrojs/compiler": ["@astrojs/compiler@3.0.0", "", {}, "sha512-MwAbDE5mawZ1SS+D8qWiHdprdME5Tlj2e0YjxnEICvcOpbSukNS7Sa7hA5PK+6RrmUr/t6Gi5YgrdZKjbO/WPQ=="], + "@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.8.0", "", { "dependencies": { "picomatch": "^4.0.3" } }, "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w=="], @@ -836,6 +837,8 @@ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier-plugin-astro": ["prettier-plugin-astro@0.14.1", "", { "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", "sass-formatter": "^0.7.6" } }, "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -892,12 +895,16 @@ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], "sanitize-html": ["sanitize-html@2.17.1", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^8.0.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-ehFCW+q1a4CSOWRAdX97BX/6/PDEkCqw7/0JXZAGQV57FQB3YOkTa/rrzHPeJ+Aghy4vZAFfWMYyfxIiB7F/gw=="], + "sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="], + "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -932,6 +939,8 @@ "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + "suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], @@ -1076,6 +1085,8 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "astro/@astrojs/compiler": ["@astrojs/compiler@3.0.0", "", {}, "sha512-MwAbDE5mawZ1SS+D8qWiHdprdME5Tlj2e0YjxnEICvcOpbSukNS7Sa7hA5PK+6RrmUr/t6Gi5YgrdZKjbO/WPQ=="], + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], diff --git a/package.json b/package.json index 2a64a86..9dd7645 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "@chrisvander/website", - "type": "module", "version": "0.0.1", "private": true, + "type": "module", "scripts": { "dev": "astro dev", "start": "astro dev", @@ -32,7 +32,8 @@ "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.1", "@types/markdown-it": "^14.1.2", - "@types/sanitize-html": "^2.16.1" + "@types/sanitize-html": "^2.16.1", + "prettier-plugin-astro": "^0.14.1" }, "overrides": { "@iconify/tools": "5.0.5", diff --git a/src/base.css b/src/base.css index 26146d1..121a671 100644 --- a/src/base.css +++ b/src/base.css @@ -48,7 +48,6 @@ } @media (prefers-color-scheme: dark) { - .astro-code, .astro-code span { color: var(--shiki-dark) !important; diff --git a/src/content/blog/borrowing-network-effects.md b/src/content/blog/borrowing-network-effects.md index d16f51a..7129b99 100644 --- a/src/content/blog/borrowing-network-effects.md +++ b/src/content/blog/borrowing-network-effects.md @@ -10,7 +10,7 @@ Network effects. They are the main reason that most social media startups, both Having users on a social media network is the only way for a social media network to work. Without sufficient network density, there isn’t an incentive to keep opening the app, which stalls engagement and stalls growth. It’s for this reason that apps like Meta have chased engagement above all else - resulting in a hyper-polarized, centrally-controlled social empire dominated by just a few parties. -When Meta launched Threads, they did something interesting. They cross-advertised that your Instagram account was *already* a Threads account - all you had to do was grab the app and sign in. For this reason, Threads quickly eclipsed 100 million daily active users in December of 2024[^1]. They took an existing network, with network effect in full force, and simply reused it. +When Meta launched Threads, they did something interesting. They cross-advertised that your Instagram account was _already_ a Threads account - all you had to do was grab the app and sign in. For this reason, Threads quickly eclipsed 100 million daily active users in December of 2024[^1]. They took an existing network, with network effect in full force, and simply reused it. This brings me back to an experience I had this morning. I opened a new app called [Popfeed](https://popfeed.social/); it feels like a mashup of Letterboxd and Goodreads, but built AT Protocol. I signed in with my AT Protocol account. Lo and behold, all the people I follow on Bluesky were just... there! My feed was already filled with people that I recognized, interacting and rating movies. And unlike Threads, Bluesky doesn’t own Popfeed - Popfeed was just built on AT Protocol. diff --git a/src/content/blog/owning-your-data.md b/src/content/blog/owning-your-data.md index 1e4e408..fd92aac 100644 --- a/src/content/blog/owning-your-data.md +++ b/src/content/blog/owning-your-data.md @@ -10,15 +10,15 @@ Bluesky has decoupled identity, data, and apps to decentralize social media. It Email has endured as the Internet’s default communication layer despite countless attempts to replace it. Its dominance is explained for one reason: it runs on a standard, widely adopted protocol. Every company, big and small, has to adhere to the contract that the email protocol defines. It levels the playing field. -In the decentralized social media race, there are two primary contenders: Bluesky and Mastodon. I’ve been very excited by developments in both. I don’t think it’s a winner-take-all game. However, Mastodon feels very traditional, where data and identity are tied to the server. It makes Mastodon feel like a set of separate islands instead of one large network with pluggable backends. Mastodon, the app, cannot exist without Mastodon, the data store, which cannot exist without Mastodon, the identity provider. The user concern of “what server am I on?” is ever-present when using Mastodon. +In the decentralized social media race, there are two primary contenders: Bluesky and Mastodon. I’ve been very excited by developments in both. I don’t think it’s a winner-take-all game. However, Mastodon feels very traditional, where data and identity are tied to the server. It makes Mastodon feel like a set of separate islands instead of one large network with pluggable backends. Mastodon, the app, cannot exist without Mastodon, the data store, which cannot exist without Mastodon, the identity provider. The user concern of “what server am I on?” is ever-present when using Mastodon. In contrast, Bluesky defines a protocol called [AT Protocol](https://atproto.com/). It uses [Personal Data Servers](https://github.com/bluesky-social/pds) (or PDSs) to store user data across the network. Like its name describes, it stores user’s personal data. Anyone can run a PDS and store data for many users without needing to be concerned with running a “Bluesky instance”. Bluesky, the app, is simply a view over the set of all active personal data servers. Mastodon’s backend runs on the instance that your data is on, using an identity tied to that instance. In contrast, Bluesky’s backend exists separately, using data from the server that your data is on, through an identity that can be moved and tied to any other server. -I believe this could become the dominant way to build applications, whether we’re talking social apps or enterprise-grade apps. This paradigm allows anyone to build a “Bluesky alternative” - they, too, can build an app view that references a user’s personal data server, retrieving and storing data, on behalf of the user, in a data store the user controls. +I believe this could become the dominant way to build applications, whether we’re talking social apps or enterprise-grade apps. This paradigm allows anyone to build a “Bluesky alternative” - they, too, can build an app view that references a user’s personal data server, retrieving and storing data, on behalf of the user, in a data store the user controls. -Individuals and enterprises alike want the same thing: a stable, portable identity, and a durable data store that they control. Both individuals and businesses must be able to own their data. Apps, like Facebook, Instagram, Twitter, Reddit, should all retrieve and store data using your data server and using your consistent Internet identity. +Individuals and enterprises alike want the same thing: a stable, portable identity, and a durable data store that they control. Both individuals and businesses must be able to own their data. Apps, like Facebook, Instagram, Twitter, Reddit, should all retrieve and store data using your data server and using your consistent Internet identity. Today, enterprises integrate a mess of SaaS apps together, where one isolated data store talks to another isolated data store with zero centralization. Once users expect to own their data, enterprises will demand the same architecture. And with a PDS-style architecture, integrations wouldn’t need to exist as they do today - all the data is in the PDS, ready to be used by whatever app needs it. IT departments could finally have a clear view into what data is being retrieved, displayed, and written by every app in the organization. diff --git a/src/content/blog/ts-mapped-types.md b/src/content/blog/ts-mapped-types.md index a4ac4a8..bbe9284 100644 --- a/src/content/blog/ts-mapped-types.md +++ b/src/content/blog/ts-mapped-types.md @@ -18,22 +18,22 @@ If you aren’t familiar with generics conceptually, they are a way to have type ```ts interface Box { - readonly contents: string - clone: () => Box + readonly contents: string; + clone: () => Box; } -const box: Box = { contents: "initial", clone: () => ({ ...box }) } +const box: Box = { contents: "initial", clone: () => ({ ...box }) }; ``` If we want to hold a number inside of the `Box`, we can’t. We only allow strings right now on the type. With generics, though, we can make this box invariant over the type of the contents: ```ts interface Box { - readonly contents: T - clone: () => Box + readonly contents: T; + clone: () => Box; } -const box: Box = { contents: "initial", clone: () => ({ ...box }) } +const box: Box = { contents: "initial", clone: () => ({ ...box }) }; // ^ ts: Generic type 'Box' requires 1 type argument(s). ``` @@ -41,11 +41,11 @@ Whoops! I’ve now broken my existing code, assuming that box would contain a st ```ts interface Box { - readonly contents: T - clone: () => Box + readonly contents: T; + clone: () => Box; } -const box: Box = { contents: "initial", clone: () => ({ ...box }) } +const box: Box = { contents: "initial", clone: () => ({ ...box }) }; ``` Better. But I still have to write `: Box` anywhere I want to declare a box, and `: Box` anytime I use a different type than string, which is a little irritating. @@ -55,8 +55,8 @@ Better. But I still have to write `: Box` anywhere I want to declare a box, and In TypeScript, types are inferred in many places. Type inference allows the compiler to know what type variables are without having to explicitly declare the type. For example, TypeScript knows both of these declarations are of type `{ a: number }`: ```ts -const a = { a: 1 } -const b: { a: number } = { a: 1 } +const a = { a: 1 }; +const b: { a: number } = { a: 1 }; ``` In the case of our box example, that `clone` declaration seems like it will be the same across every instantiation. So we may want to create a function that returns a `Box`. @@ -65,12 +65,12 @@ In the case of our box example, that `clone` declaration seems like it will be t function createBox(contents: string): Box { const innerBox = { contents, - clone: () => ({ ...innerBox }) - } - return innerBox + clone: () => ({ ...innerBox }), + }; + return innerBox; } -const box = createBox("initial") +const box = createBox("initial"); ``` Okay, let’s reintroduce generics. The function itself will need to have a generic parameter, in order to pass that to the return type. So it’ll look like this: @@ -79,20 +79,20 @@ Okay, let’s reintroduce generics. The function itself will need to have a gene function createBox(contents: T): Box { const innerBox = { contents, - clone: () => ({ ...innerBox }) - } - return innerBox + clone: () => ({ ...innerBox }), + }; + return innerBox; } -const box = createBox("initial") +const box = createBox("initial"); ``` This code will compile without issue, even though I didn’t explicitly say what type the generic was. I only had to declare `createBox("initial")`. That’s because TypeScript will infer generics on a function based on the parameters passed in. It inferred `"initial"` as the type `string`, and used that in place of the generic. The following three statements all return a `Box`, in order of increasing explicitness: ```ts -const b1 = createBox(1) -const b2 = createBox(1) -const b3: Box = createBox(1) +const b1 = createBox(1); +const b2 = createBox(1); +const b3: Box = createBox(1); ``` # Mapped Types @@ -103,9 +103,9 @@ Let’s start with an example. Staying solidly in the type system, here’s an e ```ts type MyFuncs = { - a: (arg: string) => number; - b: (arg: Box) => Box; -} + a: (arg: string) => number; + b: (arg: Box) => Box; +}; ``` Here’s a scenario we recently ran into at [Bluefin](https://bluefin.one). We’d like to run these functions inside of a web worker, and make it callable from the client side. This inevitably requires us to make these functions asynchronous, and wrap some kind of adapter around them. How do we do that and maintain type safety throughout? @@ -114,14 +114,14 @@ First, let’s think about the overall structure of this mapped type. We have a ```ts type FuncArgs = { - [key: string]: (...args: any[]) => any -} + [key: string]: (...args: any[]) => any; +}; ``` Let’s make sure we’re correct. TypeScript is structural, so we can see if one type is a supertype of another. In this case, `FuncArgs` should be a supertype of `MyFuncs`. ```ts -type Test = MyFuncs extends FuncArgs ? true : false +type Test = MyFuncs extends FuncArgs ? true : false; // ^ true ``` @@ -129,13 +129,13 @@ Since `FuncArgs` exists to enforce a structure, I didn’t use generics, because ```ts function makeArgsAsync(args: T): void { - // todo + // todo } makeArgsAsync({ a: (arg: string) => arg, b: (arg: Box) => arg.clone(), -}) +}); // ^ function makeArgsAsync<{ // a: (arg: string) => string; // b: (arg: Box) => Box; @@ -150,10 +150,11 @@ TypeScript has correctly inferred the input arguments to match our `MyFuncs` typ Let’s now map the return types to Promises. We can use the built-in `Parameters` and `ReturnType` utility types to extract the parameters and return type of each function. Then, we just need to reconstruct the function, with a `Promise` around the return type. To do this on one function, it might look like this: ```ts -type ToAsync any> = - (...args: Parameters) => Promise> +type ToAsync any> = ( + ...args: Parameters +) => Promise>; -type Result = ToAsync<(arg: string) => string> +type Result = ToAsync<(arg: string) => string>; // ^ type Result = (arg: string) => Promise ``` @@ -161,51 +162,50 @@ Okay, now let’s apply `ToAsync` in a mapped type. ```ts type MyFuncs = { - a: (arg: string) => number - b: (arg: Box) => Box -} + a: (arg: string) => number; + b: (arg: Box) => Box; +}; -type ToAsync any> = - (...args: Parameters) => Promise> +type ToAsync any> = ( + ...args: Parameters +) => Promise>; type MyFuncsAsync = { - [K in keyof MyFuncs]: ToAsync -} + [K in keyof MyFuncs]: ToAsync; +}; // ^ type MyFuncsAsync = { // a: ToAsync<(arg: string) => number>; // b: ToAsync<(arg: Box) => Box>; // } - ``` The statement `[K in keyof MyFuncs]` gives us a new type `K` that we can use to map into `MyFuncs`. The statement lets us use each field in `MyFuncs` as though it were a new generic argument, and TypeScript will take those results and reconstruct a new mapped type. Here’s the above, resulting in `MyFuncsAsync` again, but this time using a generic utility type: ```ts -type ToAsync any> = - (...args: Parameters) => Promise> +type ToAsync any> = ( + ...args: Parameters +) => Promise>; type MappedToAsync = { - [K in keyof T]: ToAsync -} + [K in keyof T]: ToAsync; +}; -type MyFuncsAsync = MappedToAsync +type MyFuncsAsync = MappedToAsync; ``` With this new utility type, we can finish out the signature of the `makeArgsAsync` function: ```ts -function makeArgsAsync( - args: T -): MappedToAsync { +function makeArgsAsync(args: T): MappedToAsync { // todo } const asyncArgs = makeArgsAsync({ a: (arg: string) => arg, b: (arg: Box) => arg.clone(), -}) +}); -const result = asyncArgs.a("test") +const result = asyncArgs.a("test"); // ^ Promise ``` @@ -216,31 +216,24 @@ Success! We’ve now created a mapped type that maps an object of synchronous fu The part that many resources leave out is actually implementing the runtime code while maintaining type-safety. To mentally maintain focus when working with complex types, I recommend splitting each bit of functionality that deals with a different part of the type into separate functions. Putting all the implementation inside of a for loop or mapping inside the main function can result in a confusing type hierarchy. Here’s an implementation of just making one function asynchronous: ```ts -function makeArgAsync any>( - fn: Fn -): ToAsync { +function makeArgAsync any>(fn: Fn): ToAsync { return async (...args) => { return new Promise>((resolve) => { setTimeout(() => { - resolve(fn(...args)) - }, 1000) - }) - } + resolve(fn(...args)); + }, 1000); + }); + }; } ``` Now, let’s use it. ```ts -function makeArgsAsync( - args: T -): MappedToAsync { +function makeArgsAsync(args: T): MappedToAsync { return Object.fromEntries( - Object.keys(args).map((key) => [ - key, - makeArgAsync(args[key]) - ]), - ) as MappedToAsync + Object.keys(args).map((key) => [key, makeArgAsync(args[key])]), + ) as MappedToAsync; } ``` @@ -249,8 +242,8 @@ The `as` keyword is casting in TypeScript. We’ve had to cast to `MappedToAsync Additionally, `Object.keys` does not return the type `(keyof T)[]`. Instead, it returns `string[]`. This is because the keys may differ from the structural type that the runtime object satisfies. For example: ```ts -type ObjType = { a: number } -const obj: ObjType = { a: number, b: number } +type ObjType = { a: number }; +const obj: ObjType = { a: number, b: number }; ``` `obj` satisfies the structural type of `ObjType`, but the key `b` will show up during runtime, hence the difference in type for `Object.keys`. @@ -259,4 +252,4 @@ Casting can be unavoidable when working with complex mapped types for the above # Conclusion -Mapped types, when paired with generics and utility types like `Parameters` and `ReturnType`, give TypeScript an almost magical ability to preserve structure and intent across functions. Whether you’re adapting functions for use in a worker thread or transforming types across boundaries, these tools let you do it safely and predictably. While some type assertions are necessary, especially at runtime boundaries, maintaining clear separation of logic and generic constraints go a long way. If you’re building complex systems in TypeScript, mastering mapped types is essential. \ No newline at end of file +Mapped types, when paired with generics and utility types like `Parameters` and `ReturnType`, give TypeScript an almost magical ability to preserve structure and intent across functions. Whether you’re adapting functions for use in a worker thread or transforming types across boundaries, these tools let you do it safely and predictably. While some type assertions are necessary, especially at runtime boundaries, maintaining clear separation of logic and generic constraints go a long way. If you’re building complex systems in TypeScript, mastering mapped types is essential. diff --git a/src/pages/client-metadata.json.ts b/src/pages/client-metadata.json.ts index 463921e..07c9793 100644 --- a/src/pages/client-metadata.json.ts +++ b/src/pages/client-metadata.json.ts @@ -12,5 +12,5 @@ export async function GET() { application_type: "web", dpop_bound_access_tokens: true, }), - ) + ); } diff --git a/src/pages/feed.xml.ts b/src/pages/feed.xml.ts index 2fec6ed..dac4bfb 100644 --- a/src/pages/feed.xml.ts +++ b/src/pages/feed.xml.ts @@ -1,11 +1,11 @@ -import { getCollection } from "astro:content" -import rss from "@astrojs/rss" -import MarkdownIt from "markdown-it" -import sanitizeHtml from "sanitize-html" -const parser = new MarkdownIt() +import { getCollection } from "astro:content"; +import rss from "@astrojs/rss"; +import MarkdownIt from "markdown-it"; +import sanitizeHtml from "sanitize-html"; +const parser = new MarkdownIt(); export async function GET(context: { site: string }) { - const blog = await getCollection("blog") + const blog = await getCollection("blog"); return rss({ title: "Christian van der Loo", description: "My personal blog.", @@ -19,5 +19,5 @@ export async function GET(context: { site: string }) { }), ...post.data, })), - }) + }); } diff --git a/src/pages/index.astro b/src/pages/index.astro index e38b169..d563833 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,17 +1,17 @@ --- -import { getCollection } from "astro:content" -import { Icon } from "astro-icon/components" -import dayjs from "dayjs" -import BaseLayout from "../layouts/BaseLayout.astro" +import { getCollection } from "astro:content"; +import { Icon } from "astro-icon/components"; +import dayjs from "dayjs"; +import BaseLayout from "../layouts/BaseLayout.astro"; -const posts = await getCollection("blog") +const posts = await getCollection("blog"); const postsSorted = posts .filter((post) => !post.data.draft) .sort((a, b) => { - return new Date(b.data.date).getTime() - new Date(a.data.date).getTime() - }) + return new Date(b.data.date).getTime() - new Date(a.data.date).getTime(); + }); -const projects = await getCollection("projects") +const projects = await getCollection("projects"); --- @@ -29,25 +29,27 @@ const projects = await getCollection("projects") { postsSorted.length > 0 && ( -

Posts

-
-
- {postsSorted.map((post) => ( -
- - {dayjs(post.data.date).format("DD MMM YYYY")} - - - - {post.data.title} - - -
- ))} -
+ <> +

Posts

+
+
+ {postsSorted.map((post) => ( +
+ + {dayjs(post.data.date).format("DD MMM YYYY")} + + + + {post.data.title} + + +
+ ))} +
+ ) }

Projects

diff --git a/tsconfig.json b/tsconfig.json index 9104261..d4b8b37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,18 +5,9 @@ "jsxImportSource": "react", "baseUrl": ".", "paths": { - "@images/*": [ - "public/images/*" - ] + "@images/*": ["public/images/*"] } }, - "include": [ - ".astro/types.d.ts", - "**/*", - "./worker-configuration.d.ts" - ], - "exclude": [ - "dist", - "src/lexicons" - ] -} \ No newline at end of file + "include": [".astro/types.d.ts", "**/*", "./worker-configuration.d.ts"], + "exclude": ["dist", "src/lexicons"] +} diff --git a/wrangler.jsonc b/wrangler.jsonc index 69eafba..f6439e8 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -1,13 +1,13 @@ { - "compatibility_date": "2026-03-10", - "compatibility_flags": ["global_fetch_strictly_public", "nodejs_compat"], - "name": "chrisvanderloo", - "main": "@astrojs/cloudflare/entrypoints/server", - "assets": { - "directory": "./dist", - "binding": "ASSETS" - }, - "observability": { - "enabled": true - } + "compatibility_date": "2026-03-10", + "compatibility_flags": ["global_fetch_strictly_public", "nodejs_compat"], + "name": "chrisvanderloo", + "main": "@astrojs/cloudflare/entrypoints/server", + "assets": { + "directory": "./dist", + "binding": "ASSETS", + }, + "observability": { + "enabled": true, + }, } -- 2.51.2