Skip to content

Commit f2fb714

Browse files
aster-voidclaude
andcommitted
treewide: fix TypeScript type errors in API client
- Update api-client types to match server responses (null vs undefined) - Add proper typed route helpers for Vote, File, Task, Organization, Channel - Fix unwrapResponse to handle Eden Treaty's unknown data type - Fix SignInForm constant binding error (use value instead of bind:value) - Use unwrapResponse consistently across components 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent ceb5477 commit f2fb714

File tree

17 files changed

+155
-175
lines changed

17 files changed

+155
-175
lines changed

CLAUDE.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
3838

3939
## Import Aliases
4040

41-
| Alias | Path |
42-
|-------|------|
43-
| `@` | `src/` |
41+
| Alias | Path |
42+
| ------------- | ----------------- |
43+
| `@` | `src/` |
4444
| `$components` | `src/components/` |
45-
| `@apps/{pkg}` | `apps/{pkg}/` |
45+
| `@apps/{pkg}` | `apps/{pkg}/` |
4646

4747
</architecture>
4848

@@ -80,10 +80,10 @@ import type { App } from "@apps/server";
8080

8181
const client = treaty<App>("http://localhost:8080");
8282

83-
await client.products.get(); // GET
84-
await client.products["123"].get(); // Dynamic param
85-
await client.products.get({ query: { category: "foo" }}); // Query
86-
await client.products.post({ name: "bar", price: 100 }); // POST
83+
await client.products.get(); // GET
84+
await client.products["123"].get(); // Dynamic param
85+
await client.products.get({ query: { category: "foo" } }); // Query
86+
await client.products.post({ name: "bar", price: 100 }); // POST
8787
```
8888

8989
</framework-elysia>
@@ -117,8 +117,8 @@ UI [.svelte] → controller [.svelte.ts] → processor [.svelte.ts] → utility
117117

118118
毎回の作業前、タスクの種類に応じて `docs/skills/` 内の該当ドキュメントを読む。
119119

120-
| Skill | File | Usage |
121-
|-------|------|-------|
120+
| Skill | File | Usage |
121+
| --------- | -------------------------- | -------------------- |
122122
| UI Design | `docs/skills/ui-design.md` | UI実装、デザイン判断 |
123123

124124
</skills>
Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,47 @@
11
import type { ApiClient } from "./index.ts";
22
import type {
3-
DynamicRoute,
3+
ChannelRoute,
4+
FileRoute,
45
MessagesRoute,
56
OrganizationRoute,
7+
TaskRoute,
8+
VoteRoute,
69
} from "./route-types.ts";
710

811
/**
912
* Helper functions for type-safe dynamic route access.
1013
* Eden Treaty uses Proxy to enable dynamic route access at runtime.
11-
* The functions use explicit return type annotations instead of type assertions.
1214
*/
1315

14-
export function getOrganization<T extends string>(
16+
export function getOrganization(
1517
client: ApiClient,
16-
id: T,
18+
id: string,
1719
): OrganizationRoute {
1820
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
1921
return client.organizations[id];
2022
}
2123

22-
export function getChannel<T extends string>(
23-
client: ApiClient,
24-
id: T,
25-
): DynamicRoute<unknown> {
24+
export function getChannel(client: ApiClient, id: string): ChannelRoute {
2625
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
2726
return client.channels[id];
2827
}
2928

30-
export function getMessage<T extends string>(
31-
client: ApiClient,
32-
id: T,
33-
): MessagesRoute {
29+
export function getMessage(client: ApiClient, id: string): MessagesRoute {
3430
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
3531
return client.messages[id];
3632
}
3733

38-
export function getVote<T extends string>(
39-
client: ApiClient,
40-
id: T,
41-
): DynamicRoute<unknown> {
34+
export function getVote(client: ApiClient, id: string): VoteRoute {
4235
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
4336
return client.votes[id];
4437
}
4538

46-
export function getTask<T extends string>(
47-
client: ApiClient,
48-
id: T,
49-
): DynamicRoute<unknown> {
39+
export function getTask(client: ApiClient, id: string): TaskRoute {
5040
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
5141
return client.tasks[id];
5242
}
5343

54-
export function getFile<T extends string>(
55-
client: ApiClient,
56-
id: T,
57-
): DynamicRoute<unknown> {
44+
export function getFile(client: ApiClient, id: string): FileRoute {
5845
// @ts-expect-error - Eden Treaty Proxy allows dynamic property access
5946
return client.files[id];
6047
}

apps/api-client/src/route-types.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,60 @@
33
* These types describe the structure of dynamically accessed routes.
44
*/
55

6+
import type {
7+
Channel,
8+
File,
9+
Organization,
10+
Reaction,
11+
Task,
12+
Vote,
13+
} from "./types.ts";
14+
15+
type ApiResponse<T> = Promise<{
16+
data?: T | { message: string } | null;
17+
error?: { status: unknown; value: unknown } | null;
18+
}>;
19+
620
export type DynamicRoute<T> = {
7-
get: () => Promise<{ data?: T; error?: { status: number; value: unknown } }>;
8-
patch: (
9-
body?: Partial<T>,
10-
) => Promise<{ data?: T; error?: { status: number; value: unknown } }>;
11-
delete: () => Promise<{
12-
data?: { success: boolean };
13-
error?: { status: number; value: unknown };
14-
}>;
21+
get: () => ApiResponse<T>;
22+
patch: (body?: Partial<T>) => ApiResponse<T>;
23+
delete: () => ApiResponse<{ success: boolean }>;
24+
};
25+
26+
export type VoteRoute = DynamicRoute<Vote> & {
1527
cast?: {
16-
post: (body: {
17-
votedOptions: number[];
18-
}) => Promise<{
19-
data?: unknown;
20-
error?: { status: number; value: unknown };
21-
}>;
28+
post: (body: { votedOptions: number[] }) => ApiResponse<Vote>;
2229
};
2330
};
2431

32+
export type FileRoute = DynamicRoute<File>;
33+
34+
export type TaskRoute = DynamicRoute<Task>;
35+
2536
export type OrganizationMembersRoute = {
26-
get: () => Promise<{
27-
data?: unknown;
28-
error?: { status: number; value: unknown };
29-
}>;
30-
post: (body: {
31-
userId: string;
32-
permission: string;
33-
}) => Promise<{ data?: unknown; error?: { status: number; value: unknown } }>;
37+
get: () => ApiResponse<unknown>;
38+
post: (body: { userId: string; permission: string }) => ApiResponse<unknown>;
3439
} & {
3540
[userId: string]: {
36-
delete: () => Promise<{
37-
data?: unknown;
38-
error?: { status: number; value: unknown };
39-
}>;
41+
delete: () => ApiResponse<unknown>;
4042
};
4143
};
4244

43-
export type OrganizationRoute = DynamicRoute<unknown> & {
45+
export type OrganizationRoute = DynamicRoute<Organization> & {
4446
members: OrganizationMembersRoute;
4547
};
4648

49+
export type ChannelRoute = DynamicRoute<Channel>;
50+
4751
export type MessageReactionsRoute = {
48-
get: () => Promise<{
49-
data?: unknown;
50-
error?: { status: number; value: unknown };
51-
}>;
52-
post: (body: {
53-
emoji: string;
54-
}) => Promise<{ data?: unknown; error?: { status: number; value: unknown } }>;
52+
get: () => ApiResponse<Reaction[]>;
53+
post: (body: { emoji: string }) => ApiResponse<Reaction>;
5554
} & {
56-
[emoji: string]: {
57-
delete: () => Promise<{
58-
data?: unknown;
59-
error?: { status: number; value: unknown };
60-
}>;
61-
};
55+
[emoji: string]:
56+
| {
57+
delete: () => ApiResponse<{ success: boolean }>;
58+
}
59+
| undefined;
6260
};
6361

6462
export type MessagesRoute = {

apps/api-client/src/types.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@
22

33
export interface User {
44
id: string;
5-
email: string;
6-
name?: string;
7-
emailVerified?: Date;
8-
image?: string;
5+
email?: string | null;
6+
name?: string | null;
7+
emailVerified?: Date | null;
8+
image?: string | null;
99
createdAt: Date;
1010
updatedAt: Date;
1111
}
1212

1313
export interface Organization {
1414
id: string;
1515
name: string;
16-
description?: string;
16+
description?: string | null;
1717
ownerId: string;
1818
createdAt: Date;
1919
updatedAt: Date;
20-
permission?: string;
21-
role?: string;
20+
permission?: "admin" | "member" | "visitor";
21+
role?: string | null;
2222
}
2323

2424
export interface Channel {
2525
id: string;
2626
name: string;
27-
description?: string;
27+
description?: string | null;
2828
organizationId: string;
2929
createdAt: Date;
3030
updatedAt: Date;
@@ -36,8 +36,8 @@ export interface Message {
3636
content: string;
3737
author: string;
3838
userId: string;
39-
parentId?: string;
40-
voteId?: string;
39+
parentId?: string | null;
40+
voteId?: string | null;
4141
createdAt: Date;
4242
updatedAt: Date;
4343
// Extended fields (may be populated by joins or separate queries)

apps/desktop/src/components/chat/Personalization.svelte

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import type { User } from "@apps/api-client";
3-
import { getApiClient, useQuery } from "@/lib/api.svelte";
3+
import { getApiClient, unwrapResponse, useQuery } from "@/lib/api.svelte";
44
55
// organizationId not used in this component currently
66
// const { organizationId }: { organizationId: string } = $props();
@@ -9,17 +9,11 @@
99
1010
const identity = useQuery<User>(async () => {
1111
const res = await api.users.me.get();
12-
if (!res.data) {
13-
throw new Error("No user data returned");
14-
}
15-
return res.data;
12+
return unwrapResponse<User>(res);
1613
});
1714
const personalization = useQuery<User>(async () => {
1815
const res = await api.users.me.get();
19-
if (!res.data) {
20-
throw new Error("No user data returned");
21-
}
22-
return res.data;
16+
return unwrapResponse<User>(res);
2317
}); // TODO: Replace with actual personalization endpoint
2418
let iconURL = $state<string | null>("");
2519
let imageURL = $derived(iconURL || identity.data?.image);

apps/desktop/src/components/chat/ReactionButtons.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@
3838
const removeReaction = useMutation(
3939
async ({ messageId: mid, emoji }: { messageId: string; emoji: string }) => {
4040
const messageRoute = getMessage(api, mid);
41-
const response = await messageRoute.reactions[emoji].delete();
41+
const emojiRoute = messageRoute.reactions[emoji];
42+
if (!emojiRoute) {
43+
throw new Error(`Reaction route for ${emoji} not found`);
44+
}
45+
const response = await emojiRoute.delete();
4246
return unwrapResponse(response);
4347
},
4448
);

apps/desktop/src/components/chat/messageInputApi.svelte.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,16 @@ export class MessageInputApi {
3838
* Returns undefined if creation fails.
3939
*/
4040
async createVote(vote: Vote): Promise<string | undefined> {
41-
const voteResponse = await this.api.votes.post({
42-
title: vote.title,
43-
maxVotes: vote.maxVotes,
44-
voteOptions: vote.voteOptions,
45-
});
46-
47-
if (!voteResponse.error && voteResponse.data) {
48-
return voteResponse.data.id;
41+
try {
42+
const voteResponse = await this.api.votes.post({
43+
title: vote.title,
44+
maxVotes: vote.maxVotes,
45+
voteOptions: vote.voteOptions,
46+
});
47+
const data = unwrapResponse<{ id: string }>(voteResponse);
48+
return data.id;
49+
} catch {
50+
return undefined;
4951
}
50-
return undefined;
5152
}
5253
}

apps/desktop/src/components/chat/messageList.controller.svelte.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export class MessageListController {
2727
reactionPaletteVisibleFor = $state<string | null>(null);
2828

2929
// Mutations
30-
addReaction: ReturnType<typeof useMutation>;
30+
addReaction: ReturnType<
31+
typeof useMutation<{ messageId: string; emoji: string }, unknown>
32+
>;
3133

3234
constructor(props: () => { organizationId: string; channelId: string }) {
3335
const api = getApiClient();

apps/desktop/src/features/files/upload/uploader.svelte.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getApiClient } from "@/lib/api.svelte";
1+
import { getApiClient, unwrapResponse } from "@/lib/api.svelte";
22

33
export const MAX_FILES = 10;
44
// constants
@@ -75,35 +75,22 @@ export class FileUploader {
7575
}
7676

7777
private async uploadFile(file: File): Promise<UploadedFile> {
78-
// For now, create a simple file upload directly via the API
79-
// In a real implementation, this would use a proper storage service
8078
const response = await this.api.files.post({
81-
storageId: `temp-${Date.now()}-${Math.random()}`, // Temporary storage ID
79+
storageId: `temp-${Date.now()}-${Math.random()}`,
8280
filename: file.name,
8381
originalFilename: file.name,
8482
mimeType: file.type,
8583
size: file.size,
8684
organizationId: this.organizationId,
8785
});
8886

89-
if (response.error) {
90-
throw new Error(
91-
typeof response.error.value === "string"
92-
? response.error.value
93-
: JSON.stringify(response.error.value),
94-
);
95-
}
96-
97-
if (!response.data) {
98-
throw new Error("No file data returned");
99-
}
100-
87+
const data = unwrapResponse<UploadedFile>(response);
10188
return {
102-
id: response.data.id,
103-
filename: response.data.filename,
104-
originalFilename: response.data.originalFilename,
105-
mimeType: response.data.mimeType,
106-
size: response.data.size,
89+
id: data.id,
90+
filename: data.filename,
91+
originalFilename: data.originalFilename,
92+
mimeType: data.mimeType,
93+
size: data.size,
10794
};
10895
}
10996
}

0 commit comments

Comments
 (0)