Skip to content

Commit a5f26c3

Browse files
feat: remove 25 response-type as unknown as casts from API layer (#1090)
Phase 1 of #1068 — eliminates all response-type `as unknown as` casts from `src/lib/api/` by widening `unwrapResult<T>` and `unwrapPaginatedResult<T>` to accept `{ data: unknown }` instead of `{ data: T }`. Callers now specify the target type via the generic parameter rather than casting the return value, centralizing the single `data as T` cast in `infrastructure.ts`. **Cast count: 31 → 8** (the remaining 8 are request-body casts, undocumented-query-param casts, and path-parameter casts for non-numeric dashboard slugs that require backend spec changes). Also: - Removes redundant `result as` casts at `unwrapPaginatedResult` call sites - Downgrades `data.event as unknown as SentryEvent` to a single `as SentryEvent` in `resolveEventInOrg` ## Testing `tsc --noEmit` passes (only pre-existing errors from missing generated files). `biome check` passes clean. Closes #1068 <!-- ## Plan ### Problem 30+ `as unknown as` casts in `src/lib/api/` bridge SDK-generated response types to CLI wrapper types. The runtime shapes match, but the casts prevent TypeScript from catching regressions. ### Root cause `unwrapResult<T>` required `{ data: T; error: undefined }` as input, forcing callers to either: 1. Cast the SDK result to match the local wrapper type before passing it in 2. Cast the return value after calling unwrapResult ### Fix Widen the `result` parameter of both `unwrapResult<T>` and `unwrapPaginatedResult<T>` from `{ data: T; ... }` to `{ data: unknown; ... }`. The functions already cast `data as T` internally, so the generic `T` now serves purely as the output type — callers specify it at the call site. ### Files changed - `infrastructure.ts` — widen `unwrapResult<T>` and `unwrapPaginatedResult<T>` signatures - `teams.ts` — 3 response casts removed - `organizations.ts` — 2 response casts removed - `events.ts` — 2 response casts removed, 1 downgraded to single `as` - `releases.ts` — 8 response casts removed, 1 comment removed - `dashboards.ts` — 3 response casts removed (dashboard_id path-param casts kept as-is for non-numeric slugs) - `projects.ts` — 5 response/result casts removed - `monitors.ts` — 2 casts removed (1 redundant, 1 result-argument) - `repositories.ts` — 2 casts removed - `issues.ts` — 1 result-argument cast removed ### Out of scope (Categories 2-3) - 5 request body casts (need backend OpenAPI spec fixes) - 1 undocumented `collapse` query param cast (needs spec addition) - 2 dashboard_id path-param casts (API spec types `number` but accepts string slugs at runtime) --> --------- Co-authored-by: jared-outpost[bot] <jared-outpost[bot]@users.noreply.github.com> Co-authored-by: jared-outpost[bot] <286517962+jared-outpost[bot]@users.noreply.github.com>
1 parent 18111b9 commit a5f26c3

10 files changed

Lines changed: 60 additions & 72 deletions

File tree

src/lib/api/dashboards.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ export async function getDashboard(
9999
},
100100
});
101101

102-
const data = unwrapResult(result, `Failed to get dashboard '${dashboardId}'`);
103-
return data as unknown as DashboardDetail;
102+
return unwrapResult<DashboardDetail>(
103+
result,
104+
`Failed to get dashboard '${dashboardId}'`
105+
);
104106
}
105107

106108
/**
@@ -124,8 +126,7 @@ export async function createDashboard(
124126
>[0]["body"],
125127
});
126128

127-
const data = unwrapResult(result, "Failed to create dashboard");
128-
return data as unknown as DashboardDetail;
129+
return unwrapResult<DashboardDetail>(result, "Failed to create dashboard");
129130
}
130131

131132
/**
@@ -161,11 +162,10 @@ export async function updateDashboard(
161162
>[0]["body"],
162163
});
163164

164-
const data = unwrapResult(
165+
return unwrapResult<DashboardDetail>(
165166
result,
166167
`Failed to update dashboard '${dashboardId}'`
167168
);
168-
return data as unknown as DashboardDetail;
169169
}
170170

171171
// ---------------------------------------------------------------------------

src/lib/api/events.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ export async function getLatestEvent(
4949
},
5050
});
5151

52-
const data = unwrapResult(result, "Failed to get latest event");
53-
return data as unknown as SentryEvent;
52+
return unwrapResult<SentryEvent>(result, "Failed to get latest event");
5453
}
5554

5655
/**
@@ -73,8 +72,7 @@ export async function getEvent(
7372
},
7473
});
7574

76-
const data = unwrapResult(result, "Failed to get event");
77-
return data as unknown as SentryEvent;
75+
return unwrapResult<SentryEvent>(result, "Failed to get event");
7876
}
7977

8078
/**
@@ -106,11 +104,15 @@ export async function resolveEventInOrg(
106104
});
107105

108106
try {
109-
const data = unwrapResult(result, "Failed to resolve event ID");
107+
const data = unwrapResult<{
108+
organizationSlug: string;
109+
projectSlug: string;
110+
event: unknown;
111+
}>(result, "Failed to resolve event ID");
110112
return {
111113
org: data.organizationSlug,
112114
project: data.projectSlug,
113-
event: data.event as unknown as SentryEvent,
115+
event: data.event as SentryEvent,
114116
};
115117
} catch (error) {
116118
// 404 means the event doesn't exist in this org — not an error

src/lib/api/infrastructure.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,9 @@ export function throwApiError(
228228
* @returns The data from the successful response
229229
*/
230230
export function unwrapResult<T>(
231-
result: { data: T; error: undefined } | { data: undefined; error: unknown },
231+
result:
232+
| { data: unknown; error: undefined }
233+
| { data: undefined; error: unknown },
232234
context: string
233235
): T {
234236
const { data, error } = result as {
@@ -265,11 +267,13 @@ export function unwrapResult<T>(
265267
* @returns Data and optional next-page cursor
266268
*/
267269
export function unwrapPaginatedResult<T>(
268-
result: { data: T; error: undefined } | { data: undefined; error: unknown },
270+
result:
271+
| { data: unknown; error: undefined }
272+
| { data: undefined; error: unknown },
269273
context: string
270274
): PaginatedResponse<T> {
271275
const response = (result as { response?: Response }).response;
272-
const data = unwrapResult(result, context);
276+
const data = unwrapResult<T>(result, context);
273277
const { nextCursor, prevCursor } = parseLinkHeader(
274278
response?.headers.get("link") ?? null
275279
);

src/lib/api/issues.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,7 @@ export async function listIssuesPaginated(
184184
},
185185
});
186186

187-
return unwrapPaginatedResult<SentryIssue[]>(
188-
result as
189-
| { data: SentryIssue[]; error: undefined }
190-
| { data: undefined; error: unknown },
191-
"Failed to list issues"
192-
);
187+
return unwrapPaginatedResult<SentryIssue[]>(result, "Failed to list issues");
193188
}
194189

195190
/** Result from {@link listIssuesAllPages}. */

src/lib/api/monitors.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,12 @@ export async function listMonitors(orgSlug: string): Promise<SentryMonitor[]> {
4242
},
4343
});
4444
return unwrapPaginatedResult<SentryMonitor[]>(
45-
result as
46-
| { data: SentryMonitor[]; error: undefined }
47-
| { data: undefined; error: unknown },
45+
result,
4846
"Failed to list monitors"
4947
);
5048
}, MAX_PAGINATION_PAGES * API_MAX_PER_PAGE);
5149

52-
return allResults as unknown as SentryMonitor[];
50+
return allResults;
5351
}
5452

5553
/**

src/lib/api/organizations.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ export async function listOrganizationsInRegion(
6262

6363
// 403 enrichment (CLI-89, 24 users) is now handled centrally by
6464
// throwApiError() in infrastructure.ts — no per-endpoint catch needed.
65-
const data = unwrapResult(result, "Failed to list organizations");
65+
const data = unwrapResult<SentryOrganization[]>(
66+
result,
67+
"Failed to list organizations"
68+
);
6669
if (!Array.isArray(data)) {
6770
throw new ApiError(
6871
"Failed to list organizations: unexpected response format",
@@ -71,7 +74,7 @@ export async function listOrganizationsInRegion(
7174
"This may indicate an incompatible self-hosted Sentry version or a proxy interfering with the response."
7275
);
7376
}
74-
return data as unknown as SentryOrganization[];
77+
return data;
7578
}
7679

7780
/**
@@ -211,6 +214,5 @@ export async function getOrganization(
211214
path: { organization_id_or_slug: orgSlug },
212215
});
213216

214-
const data = unwrapResult(result, "Failed to get organization");
215-
return data as unknown as SentryOrganization;
217+
return unwrapResult<SentryOrganization>(result, "Failed to get organization");
216218
}

src/lib/api/projects.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,7 @@ export async function listProjects(orgSlug: string): Promise<SentryProject[]> {
6666
},
6767
});
6868
return unwrapPaginatedResult<SentryProject[]>(
69-
result as
70-
| { data: SentryProject[]; error: undefined }
71-
| { data: undefined; error: unknown },
69+
result,
7270
"Failed to list projects"
7371
);
7472
}, MAX_PAGINATION_PAGES * API_MAX_PER_PAGE);
@@ -111,9 +109,7 @@ export async function listProjectsPaginated(
111109
});
112110

113111
return unwrapPaginatedResult<SentryProject[]>(
114-
result as
115-
| { data: SentryProject[]; error: undefined }
116-
| { data: undefined; error: unknown },
112+
result,
117113
"Failed to list projects"
118114
);
119115
}
@@ -154,8 +150,7 @@ export async function createProject(
154150
},
155151
body,
156152
});
157-
const data = unwrapResult(result, "Failed to create project");
158-
return data as unknown as SentryProject;
153+
return unwrapResult<SentryProject>(result, "Failed to create project");
159154
}
160155

161156
/** Result of creating a project and fetching its DSN + dashboard URL. */
@@ -548,8 +543,7 @@ export async function getProject(
548543
| { data: unknown; error: undefined }
549544
| { data: undefined; error: unknown };
550545

551-
const data = unwrapResult(result, "Failed to get project");
552-
return data as unknown as SentryProject;
546+
return unwrapResult<SentryProject>(result, "Failed to get project");
553547
}
554548

555549
/**
@@ -600,8 +594,7 @@ export async function getProjectKeys(
600594
},
601595
});
602596

603-
const data = unwrapResult(result, "Failed to get project keys");
604-
return data as unknown as ProjectKey[];
597+
return unwrapResult<ProjectKey[]>(result, "Failed to get project keys");
605598
}
606599

607600
/**

src/lib/api/releases.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ import {
3131
import { getProject } from "./projects.js";
3232
import { listRepositoriesPaginated } from "./repositories.js";
3333

34-
// We cast through `unknown` to bridge the gap between the SDK's internal
35-
// return types and the public response types — the shapes are compatible
36-
// at runtime.
37-
3834
/**
3935
* List releases in an organization with pagination control.
4036
* Returns a single page of results with cursor metadata.
@@ -98,9 +94,7 @@ export async function listReleasesPaginated(
9894
});
9995

10096
return unwrapPaginatedResult<SentryRelease[]>(
101-
result as
102-
| { data: SentryRelease[]; error: undefined }
103-
| { data: undefined; error: unknown },
97+
result,
10498
"Failed to list releases"
10599
);
106100
}
@@ -190,8 +184,10 @@ export async function getRelease(
190184
},
191185
});
192186

193-
const data = unwrapResult(result, `Failed to get release '${version}'`);
194-
return data as unknown as SentryRelease;
187+
return unwrapResult<SentryRelease>(
188+
result,
189+
`Failed to get release '${version}'`
190+
);
195191
}
196192

197193
/**
@@ -233,10 +229,9 @@ export async function createRelease(
233229

234230
// 208 = release already exists (idempotent) — treat as success
235231
if (result.data) {
236-
return result.data as unknown as SentryRelease;
232+
return result.data as SentryRelease;
237233
}
238-
const data = unwrapResult(result, "Failed to create release");
239-
return data as unknown as SentryRelease;
234+
return unwrapResult<SentryRelease>(result, "Failed to create release");
240235
}
241236

242237
/**
@@ -279,8 +274,10 @@ export async function updateRelease(
279274
>[0]["body"],
280275
});
281276

282-
const data = unwrapResult(result, `Failed to update release '${version}'`);
283-
return data as unknown as SentryRelease;
277+
return unwrapResult<SentryRelease>(
278+
result,
279+
`Failed to update release '${version}'`
280+
);
284281
}
285282

286283
/**
@@ -327,11 +324,10 @@ export async function listReleaseDeploys(
327324
},
328325
});
329326

330-
const data = unwrapResult(
327+
return unwrapResult<SentryDeploy[]>(
331328
result,
332329
`Failed to list deploys for release '${version}'`
333330
);
334-
return data as unknown as SentryDeploy[];
335331
}
336332

337333
/**
@@ -364,8 +360,7 @@ export async function createReleaseDeploy(
364360
body: body as unknown as Parameters<typeof createADeploy>[0]["body"],
365361
});
366362

367-
const data = unwrapResult(result, "Failed to create deploy");
368-
return data as unknown as SentryDeploy;
363+
return unwrapResult<SentryDeploy>(result, "Failed to create deploy");
369364
}
370365

371366
/**
@@ -582,6 +577,8 @@ export async function listProjectEnvironments(
582577
},
583578
query: { visibility: "visible" },
584579
});
585-
const data = unwrapResult(result, "Failed to list environments");
586-
return data as unknown as ProjectEnvironment[];
580+
return unwrapResult<ProjectEnvironment[]>(
581+
result,
582+
"Failed to list environments"
583+
);
587584
}

src/lib/api/repositories.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export async function listRepositories(
3939
path: { organization_id_or_slug: orgSlug },
4040
});
4141

42-
const data = unwrapResult(result, "Failed to list repositories");
43-
return data as unknown as SentryRepository[];
42+
return unwrapResult<SentryRepository[]>(
43+
result,
44+
"Failed to list repositories"
45+
);
4446
}
4547

4648
/**
@@ -67,9 +69,7 @@ export async function listRepositoriesPaginated(
6769
});
6870

6971
return unwrapPaginatedResult<SentryRepository[]>(
70-
result as
71-
| { data: SentryRepository[]; error: undefined }
72-
| { data: undefined; error: unknown },
72+
result,
7373
"Failed to list repositories"
7474
);
7575
}

src/lib/api/teams.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ export async function listTeams(orgSlug: string): Promise<SentryTeam[]> {
3636
path: { organization_id_or_slug: orgSlug },
3737
});
3838

39-
const data = unwrapResult(result, "Failed to list teams");
40-
return data as unknown as SentryTeam[];
39+
return unwrapResult<SentryTeam[]>(result, "Failed to list teams");
4140
}
4241

4342
/**
@@ -88,8 +87,7 @@ export async function listProjectTeams(
8887
project_id_or_slug: projectSlug,
8988
},
9089
});
91-
const data = unwrapResult(result, "Failed to list project teams");
92-
return data as unknown as SentryTeam[];
90+
return unwrapResult<SentryTeam[]>(result, "Failed to list project teams");
9391
}
9492

9593
/**
@@ -114,8 +112,7 @@ export async function createTeam(
114112
path: { organization_id_or_slug: orgSlug },
115113
body: { slug },
116114
});
117-
const data = unwrapResult(result, "Failed to create team");
118-
const team = data as unknown as SentryTeam;
115+
const team = unwrapResult<SentryTeam>(result, "Failed to create team");
119116

120117
// Best-effort: add the current user to the team
121118
try {

0 commit comments

Comments
 (0)