Skip to content

Commit 1f49066

Browse files
committed
improve typecheck
1 parent 36c84a6 commit 1f49066

File tree

2 files changed

+61
-17
lines changed

2 files changed

+61
-17
lines changed

packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { BlockProps } from '../Block';
1919
import { Blocks } from '../Blocks';
2020
import { FileIcon } from '../FileIcon';
2121
import type { TableRecordKV } from './Table';
22-
import { type VerticalAlignment, getColumnAlignment } from './utils';
22+
import { type VerticalAlignment, getColumnAlignment, isContentRef, isStringArray } from './utils';
2323

2424
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
2525
'text-left': '**:text-left text-left',
@@ -57,18 +57,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
5757
return null;
5858
}
5959

60+
// Because definition and value depends on column, we have to check typing in each case at runtime.
61+
// Validation should have been done at the API level, but we can't know typing based on `definition.type`.
62+
// OpenAPI types cannot really handle discriminated unions based on a dynamic key.
6063
switch (definition.type) {
61-
case 'checkbox':
64+
case 'checkbox': {
65+
if (value === null || typeof value !== 'boolean') {
66+
return null;
67+
}
6268
return (
6369
<Checkbox
6470
className={tcls('w-5', 'h-5')}
65-
checked={value as boolean}
71+
checked={value}
6672
disabled={true}
6773
aria-labelledby={ariaLabelledBy}
6874
/>
6975
);
76+
}
7077
case 'rating': {
71-
const rating = value as number;
78+
if (typeof value !== 'number') {
79+
return null;
80+
}
81+
const rating = value;
7282
const max = definition.max;
7383

7484
return (
@@ -107,15 +117,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
107117
</Tag>
108118
);
109119
}
110-
case 'number':
120+
case 'number': {
121+
if (typeof value !== 'number') {
122+
return null;
123+
}
111124
return (
112125
<Tag
113126
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
114127
aria-labelledby={ariaLabelledBy}
115128
>{`${value}`}</Tag>
116129
);
130+
}
117131
case 'text': {
118-
// @ts-ignore
132+
if (typeof value !== 'string') {
133+
return null;
134+
}
119135
const fragment = getNodeFragmentByName(block, value);
120136
if (!fragment) {
121137
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
@@ -148,8 +164,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
148164
);
149165
}
150166
case 'files': {
167+
if (!isStringArray(value)) {
168+
return null;
169+
}
151170
const files = await Promise.all(
152-
(value as string[]).map((fileId) =>
171+
value.map((fileId) =>
153172
context.contentContext
154173
? resolveContentRef(
155174
{
@@ -220,10 +239,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
220239
);
221240
}
222241
case 'content-ref': {
223-
const contentRef = value ? (value as ContentRef) : null;
242+
if (value === null || !isContentRef(value)) {
243+
return null;
244+
}
224245
const resolved =
225-
contentRef && context.contentContext
226-
? await resolveContentRef(contentRef, context.contentContext, {
246+
value && context.contentContext
247+
? await resolveContentRef(value, context.contentContext, {
227248
resolveAnchorText: true,
228249
iconStyle: ['mr-2', 'text-tint-subtle'],
229250
})
@@ -238,11 +259,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
238259
<StyledLink
239260
href={resolved.href}
240261
insights={
241-
contentRef
262+
value
242263
? {
243264
type: 'link_click',
244265
link: {
245-
target: contentRef,
266+
target: value,
246267
position: SiteInsightsLinkPosition.Content,
247268
},
248269
}
@@ -256,8 +277,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
256277
);
257278
}
258279
case 'users': {
280+
if (!isStringArray(value)) {
281+
return null;
282+
}
259283
const resolved = await Promise.all(
260-
(value as string[]).map(async (userId) => {
284+
value.map(async (userId) => {
261285
const contentRef: ContentRefUser = {
262286
kind: 'user',
263287
user: userId,
@@ -294,10 +318,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
294318
);
295319
}
296320
case 'select': {
321+
if (!isStringArray(value)) {
322+
return null;
323+
}
297324
return (
298325
<Tag aria-labelledby={ariaLabelledBy}>
299326
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
300-
{(value as string[]).map((selectId) => {
327+
{value.map((selectId) => {
301328
const option = definition.options.find(
302329
(option) => option.value === selectId
303330
);
@@ -328,13 +355,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
328355
);
329356
}
330357
case 'image': {
331-
const contentRef = value ? (value as ContentRef) : null;
332-
if (!contentRef) {
358+
if (!isContentRef(value)) {
333359
return null;
334360
}
335361

336362
const image = context.contentContext
337-
? await resolveContentRef(contentRef, context.contentContext)
363+
? await resolveContentRef(value, context.contentContext)
338364
: null;
339365

340366
if (!image) {

packages/gitbook/src/components/DocumentView/Table/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,21 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
123123

124124
return 'self-center';
125125
}
126+
127+
/**
128+
* Check if a value is a ContentRef.
129+
* @param ref The value to check.
130+
* @returns True if the value is a ContentRef, false otherwise.
131+
*/
132+
export function isContentRef(ref?: DocumentTableRecord['values'][string]): ref is ContentRef {
133+
return Boolean(ref && typeof ref === 'object' && 'kind' in ref);
134+
}
135+
136+
/**
137+
* Check if a value is an array of strings.
138+
* @param value The value to check.
139+
* @returns True if the value is an array of strings, false otherwise.
140+
*/
141+
export function isStringArray(value?: DocumentTableRecord['values'][string]): value is string[] {
142+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
143+
}

0 commit comments

Comments
 (0)