Skip to content

Commit ffd0dad

Browse files
authored
Add comprehensive usage examples (#26)
1 parent f121fda commit ffd0dad

File tree

10 files changed

+236
-7
lines changed

10 files changed

+236
-7
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ jobs:
5050
- 'bun.lock'
5151
- '**/tsconfig*.json'
5252
- '**/*.ts'
53-
- '**/*.tsx'
5453
5554
lint-format:
5655
needs: [setup]

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ bun run test:coverage # Run tests with coverage report
6060

6161
```bash
6262
bun run build # Build for production
63-
bun run check:type # TypeScript type checking
63+
bun run check:type:source # TypeScript type checking
6464
```
6565

6666
## Architecture Notes

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Run these commands before submitting:
8787

8888
```bash
8989
bun run check:source # Linting and formatting must pass
90-
bun run check:type # TypeScript type checking must pass
90+
bun run check:type:source # TypeScript type checking must pass
9191
bun run test # All tests must pass
9292
bun run build # Build must succeed
9393
```
@@ -123,8 +123,8 @@ Before submitting a PR:
123123
- [ ] Confirmed change does not violate the boundary
124124
- [ ] Added tests for new functionality
125125
- [ ] Linting and formatting passes (`bun run check:source`)
126+
- [ ] Type checking passes (`bun run check:type:source`)
126127
- [ ] All tests pass (`bun run test`)
127-
- [ ] Type checking passes (`bun run check:type`)
128128
- [ ] Build succeeds (`bun run build`)
129129

130130
### PR Description

examples/00-basic.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { parse } from "#safe-formdata";
2+
3+
/**
4+
* Minimal example
5+
* - basic fields
6+
* - validation guard
7+
* - explicit type narrowing for FormData values
8+
*/
9+
10+
const formData = new FormData();
11+
formData.set("name", "Alice");
12+
formData.set("age", "20");
13+
14+
const result = parse(formData);
15+
16+
if (result.data === null) {
17+
// Validation failed
18+
console.error(result.issues);
19+
throw new Error("Invalid FormData");
20+
}
21+
22+
// From here on, result.data is non-null,
23+
// but it is typed as Record<string, string | File>.
24+
const data = result.data;
25+
26+
// name: string | File
27+
const name = data["name"];
28+
if (typeof name !== "string") {
29+
throw new Error("Expected name to be a string");
30+
}
31+
name.toUpperCase();
32+
33+
// age: string | File
34+
const age = data["age"];
35+
if (typeof age !== "string") {
36+
throw new Error("Expected age to be a string");
37+
}
38+
Number(age).toFixed(0);

examples/01-file-upload.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { parse } from "#safe-formdata";
2+
3+
/**
4+
* File upload example
5+
* - handling File values from FormData
6+
* - explicit type narrowing with instanceof File
7+
* - size and type checks
8+
*/
9+
10+
const formData = new FormData();
11+
12+
// In real usage, this usually comes from a multipart/form-data request
13+
formData.set(
14+
"avatar",
15+
new File(["dummy image data"], "avatar.png", {
16+
type: "image/png",
17+
}),
18+
);
19+
20+
const result = parse(formData);
21+
22+
if (result.data === null) {
23+
// Validation failed
24+
console.error(result.issues);
25+
throw new Error("Invalid FormData");
26+
}
27+
28+
// From here on, result.data is non-null,
29+
// but values are still typed as string | File.
30+
const data = result.data;
31+
32+
// avatar: string | File
33+
const avatar = data["avatar"];
34+
35+
// Narrow to File before accessing file-specific properties
36+
if (!(avatar instanceof File)) {
37+
throw new Error("Expected avatar to be a File");
38+
}
39+
40+
// File-specific checks
41+
const maxSizeInBytes = 2 * 1024 * 1024; // 2MB
42+
43+
if (avatar.size > maxSizeInBytes) {
44+
throw new Error("File is too large");
45+
}
46+
47+
if (avatar.type !== "image/png") {
48+
throw new Error("Unsupported file type");
49+
}
50+
51+
// Safe to use as File here

examples/02-field-presence.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { parse } from "#safe-formdata";
2+
3+
/**
4+
* Field presence example
5+
* - fields may or may not exist
6+
* - there is no "optional" field
7+
* - presence must be checked explicitly
8+
*/
9+
10+
const formData = new FormData();
11+
12+
// Simulate a partially filled form
13+
formData.set("username", "alice");
14+
15+
const result = parse(formData);
16+
17+
if (result.data === null) {
18+
console.error(result.issues);
19+
throw new Error("Invalid FormData");
20+
}
21+
22+
// result.data is non-null,
23+
// but keys are not guaranteed to exist.
24+
const data = result.data;
25+
26+
// Check field presence explicitly
27+
if (!("email" in data)) {
28+
// The field was not sent at all
29+
console.log("email was not provided");
30+
} else {
31+
const email = data["email"];
32+
33+
// Even if present, the type is still string | File
34+
if (typeof email !== "string") {
35+
throw new Error("Expected email to be a string");
36+
}
37+
38+
email.toLowerCase();
39+
}
40+
41+
// username is present
42+
const username = data["username"];
43+
if (typeof username !== "string") {
44+
throw new Error("Expected username to be a string");
45+
}
46+
47+
username.toUpperCase();

examples/03-error-handling.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { parse } from "#safe-formdata";
2+
3+
/**
4+
* Error handling example
5+
* - how to inspect validation issues
6+
* - data is null when validation fails
7+
* - duplicated keys are rejected
8+
*/
9+
10+
const formData = new FormData();
11+
12+
// This intentionally creates an invalid FormData.
13+
// safe-formdata does NOT allow duplicated keys.
14+
formData.append("role", "admin");
15+
formData.append("role", "user");
16+
17+
const result = parse(formData);
18+
19+
if (result.data !== null) {
20+
// This should never happen in this example
21+
throw new Error("Expected validation to fail");
22+
}
23+
24+
// data is null when validation fails
25+
console.log("Validation failed");
26+
27+
// issues contains detailed information about what went wrong
28+
for (const issue of result.issues) {
29+
// Issues are structured data.
30+
// Interpret or format them at a higher layer if needed.
31+
console.error({
32+
code: issue.code,
33+
path: issue.path,
34+
key: issue.key,
35+
});
36+
}
37+
38+
// Typical handling pattern:
39+
// - log issues
40+
// - return HTTP 400
41+
// - show a validation error to the user
42+
throw new Error("Invalid FormData");

examples/04-integration-fetch.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { parse } from "#safe-formdata";
2+
3+
/**
4+
* Integration example with Fetch / Request
5+
* - how to use safe-formdata in a real request handler
6+
* - parse FormData from Request
7+
* - handle missing / invalid fields
8+
*/
9+
10+
async function handleRequest(request: Request) {
11+
// Parse incoming FormData
12+
const formData = await request.formData();
13+
14+
const result = parse(formData);
15+
16+
if (result.data === null) {
17+
// Validation failed
18+
console.error("Validation issues:", result.issues);
19+
return new Response("Invalid FormData", { status: 400 });
20+
}
21+
22+
// result.data is non-null here
23+
const data = result.data;
24+
25+
// Example: retrieve 'username' field safely
26+
if (!("username" in data)) {
27+
return new Response("Username is required", { status: 400 });
28+
}
29+
30+
const username = data["username"];
31+
if (typeof username !== "string") {
32+
return new Response("Username must be a string", { status: 400 });
33+
}
34+
35+
// Proceed with valid data
36+
return new Response(`Hello, ${username.toUpperCase()}`, { status: 200 });
37+
}
38+
39+
// Dummy call to satisfy lint / tsc
40+
void handleRequest(new Request("https://example.com"));

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"types": "./src/*.ts",
1717
"import": "./src/*.ts",
1818
"default": "./src/*.ts"
19-
}
19+
},
20+
"#safe-formdata": "./src/index.ts"
2021
},
2122
"type": "module",
2223
"main": "./dist/index.js",
@@ -36,7 +37,6 @@
3637
"dev": "tsup --watch",
3738
"lint": "oxlint . --deny-warnings",
3839
"lint:fix": "oxlint . --fix-suggestions",
39-
"check:type": "tsc --noEmit",
4040
"format:biome": "biome check --write --assist-enabled true",
4141
"format:prettier": "prettier --cache --write \"**/*.{md,yml,yaml}\"",
4242
"format": "npm-run-all2 format:biome format:prettier",
@@ -46,6 +46,9 @@
4646
"check:format": "npm-run-all2 check:biome check:prettier",
4747
"check:source": "npm-run-all2 lint check:format",
4848
"check": "bun run check:source",
49+
"check:type:source": "tsc --noEmit",
50+
"check:type:example": "tsc --project tsconfig.examples.json --noEmit",
51+
"check:type": "npm-run-all2 check:type:source check:type:example",
4952
"check:prettier:ci": "prettier --check \"**/*.{md,yml,yaml}\"",
5053
"check:format:ci": "npm-run-all2 check:biome check:prettier:ci",
5154
"check:source:ci": "npm-run-all2 lint check:format:ci",
@@ -54,7 +57,7 @@
5457
"test:coverage": "vitest run --coverage",
5558
"build": "tsup",
5659
"check:package": "publint && attw --pack . --ignore-rules cjs-resolves-to-esm",
57-
"prepublishOnly": "bun run check:type && bun run test:coverage && bun run build && bun run check:package"
60+
"prepublishOnly": "bun run check:type:source && bun run test:coverage && bun run build && bun run check:package"
5861
},
5962
"devDependencies": {
6063
"@arethetypeswrong/cli": "0.18.2",

tsconfig.examples.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"declaration": false,
6+
"declarationMap": false
7+
},
8+
"include": ["examples/**/*.ts"]
9+
}

0 commit comments

Comments
 (0)