Skip to content

Commit ff2133b

Browse files
authored
Merge pull request #8863 from marmelab/ts-record-reference-fields
Add TS support to fields
2 parents fd141cd + b914e0e commit ff2133b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+647
-369
lines changed

docs/Fields.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,4 +597,39 @@ You can find components for react-admin in third-party repositories.
597597

598598
- [OoDeLally/react-admin-clipboard-list-field](https://github.com/OoDeLally/react-admin-clipboard-list-field): a quick and customizable copy-to-clipboard field.
599599
- [MrHertal/react-admin-json-view](https://github.com/MrHertal/react-admin-json-view): JSON field and input for react-admin.
600-
- [alexgschwend/react-admin-color-picker](https://github.com/alexgschwend/react-admin-color-picker): a color field
600+
- [alexgschwend/react-admin-color-picker](https://github.com/alexgschwend/react-admin-color-picker): a color field
601+
602+
## TypeScript
603+
604+
All field components accept a generic type that describes the record. This lets TypeScript validate that the `source` prop targets an actual field of the record:
605+
606+
```tsx
607+
import * as React from "react";
608+
import { Show, SimpleShowLayout, TextField, DateField, RichTextField } from 'react-admin';
609+
610+
// Note that you shouldn't extend RaRecord for this to work
611+
type Post = {
612+
id: number;
613+
title: string;
614+
teaser: string;
615+
body: string;
616+
published_at: string;
617+
}
618+
619+
export const PostShow = () => (
620+
<Show>
621+
<SimpleShowLayout>
622+
<TextField<Post> source="title" />
623+
<TextField<Post> source="teaser" />
624+
{/* Here TS will show an error because a teasr field does not exist */}
625+
<TextField<Post> source="teasr" />
626+
<RichTextField<Post> source="body" />
627+
<DateField<Post> label="Publication date" source="published_at" />
628+
</SimpleShowLayout>
629+
</Show>
630+
);
631+
```
632+
633+
**Limitation**: You must not extend `RaRecord` for this to work. This is because `RaRecord` extends `Record<string, any>` and TypeScript would not be able to infer your types properties.
634+
635+
Specifying the record type will also allow your IDE to provide auto-completion for both the `source` and `sortBy` prop. Note that the `sortBy` prop also accepts any string.

docs/js/prism.js

Lines changed: 11 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/crm/src/companies/LogoField.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { useRecordContext } from 'react-admin';
33
import { Box } from '@mui/material';
44

5-
import { Company, Contact } from '../types';
5+
import { Company } from '../types';
66

77
const sizeInPixel = {
88
medium: 42,
@@ -14,7 +14,7 @@ export const LogoField = ({
1414
}: {
1515
size?: 'small' | 'medium';
1616
}) => {
17-
const record = useRecordContext<Company | Contact>();
17+
const record = useRecordContext<Company>();
1818
if (!record) return null;
1919
return (
2020
<Box

examples/crm/src/contacts/TagsListEdit.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ export const TagsListEdit = () => {
6161
};
6262

6363
const handleDeleteTag = (id: Identifier) => {
64-
const tags: Identifier[] = record.tags.filter(
65-
(tagId: Identifier) => tagId !== id
66-
);
64+
const tags = record.tags.filter(tagId => tagId !== id);
6765
update('contacts', {
6866
id: record.id,
6967
data: { tags },
@@ -72,7 +70,7 @@ export const TagsListEdit = () => {
7270
};
7371

7472
const handleAddTag = (id: Identifier) => {
75-
const tags: Identifier[] = [...record.tags, id];
73+
const tags = [...record.tags, id];
7674
update('contacts', {
7775
id: record.id,
7876
data: { tags },
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { RaRecord } from 'react-admin';
2-
import { Company, Contact, ContactNote, Deal, Tag } from '../types';
2+
import { Company, Contact, ContactNote, Deal, Sale, Tag } from '../types';
33

44
export interface Db {
55
companies: Company[];
66
contacts: Contact[];
77
contactNotes: ContactNote[];
88
deals: Deal[];
99
dealNotes: RaRecord[];
10-
sales: RaRecord[];
10+
sales: Sale[];
1111
tags: Tag[];
1212
tasks: RaRecord[];
1313
}

examples/crm/src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RaRecord, Identifier } from 'react-admin';
1+
import { Identifier, RaRecord } from 'react-admin';
22

33
export interface Sale extends RaRecord {
44
first_name: string;
@@ -38,6 +38,8 @@ export interface Contact extends RaRecord {
3838
gender: string;
3939
sales_id: Identifier;
4040
nb_notes: number;
41+
status: string;
42+
background: string;
4143
}
4244

4345
export interface ContactNote extends RaRecord {
@@ -59,6 +61,7 @@ export interface Deal extends RaRecord {
5961
amount: number;
6062
created_at: string;
6163
updated_at: string;
64+
start_at: string;
6265
sales_id: Identifier;
6366
index: number;
6467
nb_notes: number;

examples/demo/src/products/ProductReferenceField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface Props {
77

88
const ProductReferenceField = (
99
props: Props &
10-
Omit<Omit<ReferenceFieldProps, 'source'>, 'reference' | 'children'>
10+
Omit<ReferenceFieldProps, 'source' | 'reference' | 'children'>
1111
) => (
1212
<ReferenceField
1313
label="Product"

examples/demo/src/reviews/ReviewItem.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import {
1616
} from 'react-admin';
1717

1818
import AvatarField from '../visitors/AvatarField';
19-
import { Customer } from './../types';
19+
import { Customer, Review } from './../types';
2020

2121
export const ReviewItem = () => {
22-
const record = useRecordContext();
22+
const record = useRecordContext<Review>();
2323
const createPath = useCreatePath();
2424
if (!record) {
2525
return null;

examples/demo/src/types.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RaRecord, Identifier } from 'react-admin';
1+
import { Identifier, RaRecord } from 'react-admin';
22

33
export type ThemeName = 'light' | 'dark';
44

@@ -35,6 +35,7 @@ export interface Customer extends RaRecord {
3535
groups: string[];
3636
nb_commands: number;
3737
total_spent: number;
38+
email: string;
3839
}
3940

4041
export type OrderStatus = 'ordered' | 'delivered' | 'cancelled';
@@ -44,14 +45,22 @@ export interface Order extends RaRecord {
4445
basket: BasketItem[];
4546
date: Date;
4647
total: number;
48+
total_ex_taxes: number;
49+
delivery_fees: number;
50+
tax_rate: number;
51+
taxes: number;
52+
customer_id: Identifier;
53+
reference: string;
4754
}
4855

49-
export interface BasketItem {
56+
export type BasketItem = {
5057
product_id: Identifier;
5158
quantity: number;
52-
}
59+
};
5360

54-
export interface Invoice extends RaRecord {}
61+
export interface Invoice extends RaRecord {
62+
date: Date;
63+
}
5564

5665
export type ReviewStatus = 'accepted' | 'pending' | 'rejected';
5766

@@ -60,6 +69,7 @@ export interface Review extends RaRecord {
6069
status: ReviewStatus;
6170
customer_id: Identifier;
6271
product_id: Identifier;
72+
comment: string;
6373
}
6474

6575
declare global {

examples/demo/src/visitors/FullNameField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const FullNameField = (props: Props) => {
3838
};
3939

4040
FullNameField.defaultProps = {
41-
source: 'last_name',
41+
source: 'last_name' as const,
4242
label: 'resources.customers.fields.name',
4343
};
4444

packages/ra-core/src/controller/field/useReferenceArrayFieldController.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { useGetManyAggregate } from '../../dataProvider';
55
import { ListControllerResult, useList } from '../list';
66
import { useNotify } from '../../notification';
77

8-
export interface UseReferenceArrayFieldControllerParams {
8+
export interface UseReferenceArrayFieldControllerParams<
9+
RecordType extends RaRecord = RaRecord
10+
> {
911
filter?: any;
1012
page?: number;
1113
perPage?: number;
12-
record?: RaRecord;
14+
record?: RecordType;
1315
reference: string;
1416
resource: string;
1517
sort?: SortPayload;
@@ -43,8 +45,11 @@ const defaultSort = { field: null, order: null };
4345
*
4446
* @returns {ListControllerResult} The reference props
4547
*/
46-
export const useReferenceArrayFieldController = (
47-
props: UseReferenceArrayFieldControllerParams
48+
export const useReferenceArrayFieldController = <
49+
RecordType extends RaRecord = RaRecord,
50+
ReferenceRecordType extends RaRecord = RaRecord
51+
>(
52+
props: UseReferenceArrayFieldControllerParams<RecordType>
4853
): ListControllerResult => {
4954
const {
5055
filter = defaultFilter,
@@ -64,7 +69,9 @@ export const useReferenceArrayFieldController = (
6469
return emptyArray;
6570
}, [value, source]);
6671

67-
const { data, error, isLoading, isFetching, refetch } = useGetManyAggregate(
72+
const { data, error, isLoading, isFetching, refetch } = useGetManyAggregate<
73+
ReferenceRecordType
74+
>(
6875
reference,
6976
{ ids },
7077
{
@@ -88,7 +95,7 @@ export const useReferenceArrayFieldController = (
8895
}
8996
);
9097

91-
const listProps = useList({
98+
const listProps = useList<ReferenceRecordType>({
9299
data,
93100
error,
94101
filter,

packages/ra-core/src/controller/field/useReferenceManyFieldController.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ import isEqual from 'lodash/isEqual';
55
import { useSafeSetState, removeEmpty } from '../../util';
66
import { useGetManyReference } from '../../dataProvider';
77
import { useNotify } from '../../notification';
8-
import { RaRecord, SortPayload } from '../../types';
8+
import { Identifier, RaRecord, SortPayload } from '../../types';
99
import { ListControllerResult } from '../list';
1010
import usePaginationState from '../usePaginationState';
1111
import { useRecordSelection } from '../list/useRecordSelection';
1212
import useSortState from '../useSortState';
1313
import { useResourceContext } from '../../core';
1414

15-
export interface UseReferenceManyFieldControllerParams {
15+
export interface UseReferenceManyFieldControllerParams<
16+
RecordType extends RaRecord = RaRecord
17+
> {
1618
filter?: any;
1719
page?: number;
1820
perPage?: number;
19-
record?: RaRecord;
21+
record?: RecordType;
2022
reference: string;
2123
resource?: string;
2224
sort?: SortPayload;
@@ -52,9 +54,12 @@ const defaultFilter = {};
5254
*
5355
* @returns {ListControllerResult} The reference many props
5456
*/
55-
export const useReferenceManyFieldController = (
56-
props: UseReferenceManyFieldControllerParams
57-
): ListControllerResult => {
57+
export const useReferenceManyFieldController = <
58+
RecordType extends RaRecord = RaRecord,
59+
ReferenceRecordType extends RaRecord = RaRecord
60+
>(
61+
props: UseReferenceManyFieldControllerParams<RecordType>
62+
): ListControllerResult<ReferenceRecordType> => {
5863
const {
5964
reference,
6065
record,
@@ -147,11 +152,11 @@ export const useReferenceManyFieldController = (
147152
isFetching,
148153
isLoading,
149154
refetch,
150-
} = useGetManyReference(
155+
} = useGetManyReference<ReferenceRecordType>(
151156
reference,
152157
{
153158
target,
154-
id: get(record, source),
159+
id: get(record, source) as Identifier,
155160
pagination: { page, perPage },
156161
sort,
157162
filter: filterValues,

packages/ra-core/src/controller/list/useRecordSelection.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMemo } from 'react';
22

33
import { useStore, useRemoveFromStore } from '../../store';
4-
import { Identifier } from '../../types';
4+
import { RaRecord } from '../../types';
55

66
/**
77
* Get the list of selected items for a resource, and callbacks to change the selection
@@ -10,14 +10,14 @@ import { Identifier } from '../../types';
1010
*
1111
* @returns {Object} Destructure as [selectedIds, { select, toggle, clearSelection }].
1212
*/
13-
export const useRecordSelection = (
13+
export const useRecordSelection = <RecordType extends RaRecord = any>(
1414
resource: string
1515
): [
16-
Identifier[],
16+
RecordType['id'][],
1717
{
18-
select: (ids: Identifier[]) => void;
19-
unselect: (ids: Identifier[]) => void;
20-
toggle: (id: Identifier) => void;
18+
select: (ids: RecordType['id'][]) => void;
19+
unselect: (ids: RecordType['id'][]) => void;
20+
toggle: (id: RecordType['id']) => void;
2121
clearSelection: () => void;
2222
}
2323
] => {
@@ -27,18 +27,18 @@ export const useRecordSelection = (
2727

2828
const selectionModifiers = useMemo(
2929
() => ({
30-
select: (idsToAdd: Identifier[]) => {
30+
select: (idsToAdd: RecordType['id'][]) => {
3131
if (!idsToAdd) return;
3232
setIds([...idsToAdd]);
3333
},
34-
unselect(idsToRemove: Identifier[]) {
34+
unselect(idsToRemove: RecordType['id'][]) {
3535
if (!idsToRemove || idsToRemove.length === 0) return;
3636
setIds(ids => {
3737
if (!Array.isArray(ids)) return [];
3838
return ids.filter(id => !idsToRemove.includes(id));
3939
});
4040
},
41-
toggle: (id: Identifier) => {
41+
toggle: (id: RecordType['id']) => {
4242
if (typeof id === 'undefined') return;
4343
setIds(ids => {
4444
if (!Array.isArray(ids)) return [...ids];

packages/ra-core/src/controller/record/useRecordContext.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useContext } from 'react';
22
import { RecordContext } from './RecordContext';
3+
import { RaRecord } from '../../types';
34

45
/**
56
* Hook to read the record from a RecordContext.
@@ -30,7 +31,7 @@ import { RecordContext } from './RecordContext';
3031
* @returns {RaRecord} A record object
3132
*/
3233
export const useRecordContext = <
33-
RecordType extends Record<string, unknown> = Record<string, any>
34+
RecordType extends RaRecord | Omit<RaRecord, 'id'> = RaRecord
3435
>(
3536
props?: UseRecordContextParams<RecordType>
3637
): RecordType | undefined => {
@@ -42,7 +43,7 @@ export const useRecordContext = <
4243
};
4344

4445
export interface UseRecordContextParams<
45-
RecordType extends Record<string, unknown> = Record<string, unknown>
46+
RecordType extends RaRecord | Omit<RaRecord, 'id'> = RaRecord
4647
> {
4748
record?: RecordType;
4849
[key: string]: any;

packages/ra-core/src/controller/useReference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface UseReferenceResult<RecordType extends RaRecord = any> {
4444
*
4545
* @returns {UseReferenceResult} The reference record
4646
*/
47-
export const useReference = <RecordType extends RaRecord = any>({
47+
export const useReference = <RecordType extends RaRecord = RaRecord>({
4848
reference,
4949
id,
5050
options = {},

0 commit comments

Comments
 (0)