Replace remaining any casts in shared with proper types

- ui/Table.astro: type the people rows with a local Person interface
  instead of Record<string, any>[]. Surfaced a latent type mismatch:
  <Avatar personId={person.id}> passed a string id into a prop typed
  as number (only silently worked because of the any cast) — replaced
  with <Avatar person={person}>, which resolves to the same person
  object without the redundant numeric re-lookup.
- ui/Select.astro: add SelectOption/SelectData types for selects.json
  entries instead of two Record<string, any> casts.
- components/navbar/NavbarMenu.astro: type Level1.children as
  Record<string, Level2> (matching NavbarMenuItem's own Level2/Level3
  shapes) instead of Record<string, unknown>, removing the `as any`
  casts at both NavbarMenuItem call sites.
- components/parts/Tasks.astro: add a Task interface instead of
  Record<string, any>[]; also drops the now-redundant inline subtask
  type annotation.

No behavior change other than the Table.astro Avatar fix above.
Verified: astro check 0/0/0 in shared, astro build succeeds for
preview (127 pages) and docs (128 pages), vitest passes (38 tests).
This commit is contained in:
codecalm
2026-08-08 18:11:46 +02:00
parent 0085eeba6e
commit ad42cfc76e
4 changed files with 58 additions and 9 deletions
+15 -3
View File
@@ -5,6 +5,18 @@ import NavbarMenuItem from './NavbarMenuItem.astro'
import menu from '@data/menu.json'
import menuSample from '@data/menu-sample.json'
interface Level3 {
title: string
url?: string
badge?: string
}
interface Level2 extends Level3 {
icon?: string
color?: string
children?: Record<string, Level3>
}
interface Level1 {
'title': string
'title-long'?: string
@@ -15,7 +27,7 @@ interface Level1 {
'disabled'?: boolean
'right'?: boolean
'columns'?: number
'children'?: Record<string, Record<string, unknown>>
'children'?: Record<string, Level2>
}
interface Props {
@@ -80,13 +92,13 @@ const chunk = <T,>(arr: T[], size: number): T[][] => {
{columns.map((column) => (
<div class="dropdown-menu-column">
{column.map(([childKey, child]) => (
<NavbarMenuItem level1Key={key} itemKey={childKey} item={child as any} currentPage={currentPage} keepOpen={keepOpen} />
<NavbarMenuItem level1Key={key} itemKey={childKey} item={child} currentPage={currentPage} keepOpen={keepOpen} />
))}
</div>
))}
</div>
) : (
children.map(([childKey, child]) => <NavbarMenuItem level1Key={key} itemKey={childKey} item={child as any} currentPage={currentPage} keepOpen={keepOpen} />)
children.map(([childKey, child]) => <NavbarMenuItem level1Key={key} itemKey={childKey} item={child} currentPage={currentPage} keepOpen={keepOpen} />)
)}
</div>
)}
+17 -2
View File
@@ -6,9 +6,24 @@ import AvatarList from '@ui/AvatarList.astro'
import CardTitle from '@ui/CardTitle.astro'
import tasksData from '@data/tasks.json'
interface Task {
'name': string
'color'?: string
'starred'?: boolean
'description'?: string
'image'?: string
'users'?: number
'users-offset'?: number
'due-date'?: string
'favorite'?: boolean
'likes'?: number
'subtasks'?: { name: string; done?: boolean }[]
'comments'?: number
}
interface Column {
name: string
tasks: Record<string, any>[]
tasks: Task[]
}
interface Props {
@@ -100,7 +115,7 @@ const { data = tasksData as unknown as { columns: Column[] }, class: className }
{task.subtasks && (
<div class="divide-y-2 mt-4">
{task.subtasks.map((subtask: { name: string; done?: boolean }) => (
{task.subtasks.map((subtask) => (
<div>
{subtask.done ? (
<Fragment>
+15 -2
View File
@@ -20,6 +20,19 @@ interface Props {
showSearch?: boolean;
}
interface SelectOption {
name: string;
flag?: string;
label?: string;
selected?: boolean;
}
interface SelectData {
multiple?: boolean;
data?: string;
options?: string[] | { title: string; options: string[] }[] | Record<string, SelectOption>;
}
const {
id: idProp,
key: keyProp,
@@ -35,7 +48,7 @@ const {
const id = idProp ?? keyProp;
const key = keyProp ?? 'people';
const data = (selects as Record<string, any>)[key] ?? {};
const data = (selects as Record<string, SelectData>)[key] ?? {};
const selectClass = [
'form-select',
@@ -78,7 +91,7 @@ if (values) {
options = (data.options as string[]).map((option) => ({ value: option, text: option }));
} else if (data.options) {
// object of { key: { name, flag?, label?, selected? } } — with optional indicator
options = Object.entries(data.options as Record<string, any>).map(([optKey, v]) => {
options = Object.entries(data.options as Record<string, SelectOption>).map(([optKey, v]) => {
let custom: string | undefined;
if (indicator === 'flag') custom = `<span class="flag flag-xs flag-country-${v.flag}"></span>`;
else if (indicator === 'label') custom = `<span class="badge bg-primary-lt">${v.label}</span>`;
+11 -2
View File
@@ -6,6 +6,15 @@ import DropdownMenu from './DropdownMenu.astro'
import people from '@data/people.json'
import { randomNumber } from '@shared/lib/pseudo-random'
interface Person {
id: string
full_name?: string
email?: string
job_title?: string
department?: string
[key: string]: unknown
}
interface Props {
card?: boolean
limit?: number
@@ -22,7 +31,7 @@ const { card, limit = 5, stripped, offset, avatars, mobile, buttons, nowrap } =
const roles = ['User', 'Admin', 'Owner']
const start = offset ?? 0
const rows = (people as Record<string, any>[]).slice(start, start + limit)
const rows = (people as Person[]).slice(start, start + limit)
const tableClasses = ['table', 'table-vcenter', mobile && 'table-mobile-md', card && 'card-table', stripped && 'table-striped', nowrap && 'table-nowrap']
---
@@ -52,7 +61,7 @@ const tableClasses = ['table', 'table-vcenter', mobile && 'table-mobile-md', car
{avatars ? (
<td data-label={mobile ? 'Name' : undefined}>
<div class="d-flex py-1 align-items-center">
<Avatar personId={person.id} class="me-2" />
<Avatar person={person} class="me-2" />
<div class="flex-fill">
<div class="fw-medium">{person.full_name}</div>
<div class="text-secondary">