Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const nextConfig = {

const withMDX = createMDX({
options: {
remarkPlugins: ["remark-frontmatter", "remark-emoji", "remark-prism"],
remarkPlugins: ["remark-frontmatter", "remark-emoji"],
rehypePlugins: ["rehype-slug", "rehype-autolink-headings", ["@jsdevtools/rehype-toc"]],
},
});
Expand Down
39 changes: 21 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,55 @@
"test": "yarn jest"
},
"engines": {
"node": "^22"
"node": ">=22"
},
"dependencies": {
"@rc-component/slider": "1.0.1",
"@vercel/analytics": "1.6.1",
"@vercel/analytics": "2.0.1",
"chalk": "5.6.2",
"clsx": "2.1.1",
"next": "16.0.10",
"react": "19.2.3",
"react-dom": "19.2.3"
"next": "16.2.1",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@jsdevtools/rehype-toc": "3.0.2",
"@mdx-js/loader": "3.1.1",
"@mdx-js/react": "3.1.1",
"@next/mdx": "16.0.10",
"@next/mdx": "16.2.1",
"@octokit/rest": "22.0.1",
"@tailwindcss/postcss": "4.1.18",
"@shikijs/twoslash": "3.22.0",
"@tailwindcss/postcss": "4.2.2",
"@types/jest": "30.0.0",
"@types/lodash.orderby": "4.6.9",
"@types/mdx": "2.0.13",
"@types/node": "25.0.2",
"@types/react": "19.2.7",
"@types/node": "25.5.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"cross-env": "10.1.0",
"debug": "4.4.3",
"eslint": "9.39.2",
"eslint-config-next": "16.0.10",
"eslint": "9.39.4",
"eslint-config-next": "16.2.1",
"eslint-config-prettier": "10.1.8",
"gray-matter": "4.0.3",
"jest": "30.2.0",
"lodash.orderby": "4.6.0",
"jest": "30.3.0",
"lodash.orderby": "4.18.0",
"node-emoji": "2.2.0",
"postcss": "8.5.6",
"postcss-nested": "7.0.2",
"prettier": "3.7.4",
"prettier": "3.8.1",
"prettier-plugin-tailwindcss": "0.7.2",
"rehype-autolink-headings": "7.1.0",
"rehype-raw": "7.0.0",
"rehype-slug": "6.0.0",
"rehype-stringify": "10.0.1",
"remark-emoji": "5.0.2",
"remark-frontmatter": "5.0.0",
"remark-prism": "1.3.6",
"tailwindcss": "4.1.18",
"remark-parse": "11.0.0",
"remark-rehype": "11.1.2",
"shiki": "3.22.0",
"tailwindcss": "4.2.2",
"ts-jest": "29.4.6",
"tsx": "4.21.0",
"typescript": "5.9.3"
"typescript": "6.0.2"
}
}
22 changes: 11 additions & 11 deletions src/api/projects/public_repositories.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"metadata": {
"exportDate": "2025-10-07T07:29:20.464Z"
"exportDate": "2026-02-10T09:46:34.217Z"
},
"githubRepositories": [
{
Expand All @@ -17,15 +17,15 @@
"description": "Open Rodent's Revenge is a C++ remake of the famous Microsoft game \"Rodent's Revenge\" (1991). 50k+ downloads.",
"fullName": "pierreyoda/o2r",
"forksCount": 0,
"stargazersCount": 29
"stargazersCount": 30
},
{
"url": "https://github.com/pierreyoda/micropolis-rs",
"name": "micropolis-rs",
"description": "The classic Micropolis (Sim City 1) game rewritten in Rust and React, with WebAssembly support.",
"fullName": "pierreyoda/micropolis-rs",
"forksCount": 1,
"stargazersCount": 25
"stargazersCount": 24
},
{
"url": "https://github.com/pierreyoda/rust-neuralnet",
Expand All @@ -35,20 +35,20 @@
"forksCount": 3,
"stargazersCount": 12
},
{
"url": "https://github.com/pierreyoda/GameInc",
"name": "GameInc",
"description": "Game development studio management game made with Unity. Custom homemade language for scripting.",
"fullName": "pierreyoda/GameInc",
"forksCount": 4,
"stargazersCount": 9
},
{
"url": "https://github.com/pierreyoda/rustboycolor",
"name": "rustboycolor",
"description": "Simple Game Boy (Color) emulator written in Rust.",
"fullName": "pierreyoda/rustboycolor",
"forksCount": 0,
"stargazersCount": 10
},
{
"url": "https://github.com/pierreyoda/GameInc",
"name": "GameInc",
"description": "Game development studio management game made with Unity. Custom homemade language for scripting.",
"fullName": "pierreyoda/GameInc",
"forksCount": 4,
"stargazersCount": 9
},
{
Expand Down
39 changes: 39 additions & 0 deletions src/components/blog/HighlightedCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use server";

import { codeToHtml } from "shiki";
import { transformerTwoslash } from "@shikijs/twoslash";
import { FunctionComponent, useLayoutEffect, useMemo, useState } from "react";

const SHIKI_THEME = "dark-plus";
const LANGUAGE_STRING_PRESET = "language-";

interface HighlightedCodeProps {
src: string;
lang: string;
rest: any;
}

export const HighlightedCode: FunctionComponent<HighlightedCodeProps> = ({ src, lang, rest }) => {
console.log(rest);
const language = useMemo(() => lang.substring(LANGUAGE_STRING_PRESET.length), [lang]);
const [codeHTML, setCodeHTML] = useState<TrustedHTML>("");
useLayoutEffect(() => {
(async () => {
console.log(language);
const html = await codeToHtml(src, {
lang: language,
theme: SHIKI_THEME,
transformers: ["ts", "typescript"].includes(language)
? [
transformerTwoslash({
explicitTrigger: true,
}),
]
: [],
});
setCodeHTML(html);
})();
}, [src, language]);

return <div dangerouslySetInnerHTML={{ __html: codeHTML }}></div>;
};
2 changes: 1 addition & 1 deletion src/components/blog/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// TODO: maybe setup into working remark/rehype Mdsvex plugin
// TODO: maybe setup into working remark/rehype MDX plugin

import { FunctionComponent } from "react";

Expand Down
13 changes: 13 additions & 0 deletions src/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { MDXComponents } from "mdx/types";

import { HighlightedCode } from "./components/blog/HighlightedCode";

const components = {
pre: ({
children: {
props: { children: src, className: lang, ...rest },
},
}) => <HighlightedCode src={src} lang={lang} rest={rest} />,
} satisfies MDXComponents;

export const useMDXComponents = (): MDXComponents => components;
124 changes: 124 additions & 0 deletions src/pages/blog/typescript-variadic-tuples.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: "An application of Typescript variadic tuples"
description: "In a AWS Serverless context, see how Typescript variadic tuples can help parse entry parameters in a type-safe and convenient manner."
date: "2022-06-24"
published: true
---

# An application of Typescript variadic tuples

## Typescript variadic tuples

Introduced in Typescript 4.0, variadic tuples are a way to type a given function's array parameters. TODO:

## Context

As part of working for a previous company as a R&D Software Engineer, I was asked to migrate an old C# .NET micro-service with a from-scratch one in Node.js and AWS Serverless.

To this end, the Serverless micro-service had, among other functionalities, a wide variety of endpoints each deployed as a singular AWS Lambda serverless function.
The issue was, that each of these functions required various input parameters that took a lot of the function code to process.

This led me to migrate to - at the time - the beta version of [Typescript 4.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types) which allowed me to write a quite DRY handling of input parameters, with all of the complexity handled at the Typescript typings level.

In the following chapters, we will refer to this micro-library as `parameters-lib`.

### Types of input parameters for a Lambda function

In AWS Serverless, a lambda function can have various input parameter:

- Query parameters like `?query=value`
- Multiple query parameters like `?multi=1&multi=2`
- Route parameters, just like `id=5` in the following route path: `/get/5`
- Headers in the input request

For every `type` of parameter, the value to process is found in the Lambda route's input event, which comes as follow:

```ts twoslash
// AWS typings (shortened)
export interface APIGatewayProxyEvent {
body: string | null;
// Header input values
headers: { [name: string]: string };
// Route parameters values
pathParameters: { [name: string]: string } | null;
// Query parameters values
queryStringParameters: { [name: string]: string } | null;
// Multiple query parameters values
multiValueQueryStringParameters: { [name: string]: string[] } | null;
/** ...other input event values */
}
```

In `parameters-lib`, this translates to the following code:

```typescript twoslash
export type LambdaInputEventSource =
| "headers",
| "pathParameters",
| "queryStringParameters",
| "multiValueQueryStringParameters"
;
// here, we narrow APIGatewayProxyEvent to what is of interest
export type LambdaInputEvent = Partial<Pick<APIGatewayProxyEvent, LambdaInputEventSource>>;
// ^?

// Dev-friendly names for each type of input parameter
export type LambdaEventParamType = "query" | "multi-query" | "route" | "header";
```

### Definition and dispatching of Lambda input parameters

First off, let's start with the two possible results for a given input parameter:

```typescript
export interface LambdaEventParamParserSuccess<T> {
value: T;
}

export interface LambdaEventParamParserFailure {
message: string;
}

export type LambdaEventParamParserResult<T> = LambdaEventParamParserSuccess<T> | LambdaEventParamParserFailure;
```

With this comes a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) allowing type-safe distinction between these two types:

```typescript
export const lambdaEventParamParserResultIsFailure = <T>(
result: LambdaEventParamParserResult<T>,
): result is LambdaEventParamParserFailure => !!(result as LambdaEventParamParserFailure).message;
```

We then need three types which, combined together, allow complete definition of an input parameter and its processing.

```typescript
/** Value parser for input parameters. */
export type LambdaEventParamParser<T> = (
raw: string,
type: LambdaEventParamType,
name: string,
event: LambdaInputEvent,
) => LambdaEventParamParserResult<T>;

/** Value validator for input parameters. Return true if valid. */
export type LambdaEventParamValidator<T> = (
value: T,
type: LambdaEventParamType,
name: string,
) =>
| true
| {
message: string;
};

/** Definition of an input parameter */
export interface LambdaEventParamMeta<T, Type extends LambdaEventParamType, Required extends boolean> {
name: string;
type: Type;
required: Required;
parser: LambdaEventParamParser<T>;
/** A single input parameter can have several validators. */
validators: LambdaEventParamValidator<T>[];
}
```
16 changes: 14 additions & 2 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,20 @@
@apply pl-2;
}

p > code {
@apply font-mono text-sm font-semibold;
pre > code {
@apply font-mono text-sm font-medium;

/* line numbers: https://github.com/shikijs/shiki/issues/3 */
counter-reset: step;
counter-increment: step calc(var(--start, 1) - 1);
> span::before {
content: counter(step);
counter-increment: step;
@apply w-4 mr-6 inline-block text-right text-light-orange;
&:last-child {
@apply hidden;
}
}
}

/* Table of Contents */
Expand Down
23 changes: 4 additions & 19 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"downlevelIteration": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
Expand All @@ -25,20 +20,10 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
Loading
Loading