diff --git a/docs/custom-layouts.md b/docs/custom-layouts.md index 8443758e..b26a6ba3 100644 --- a/docs/custom-layouts.md +++ b/docs/custom-layouts.md @@ -86,3 +86,59 @@ Toast.show({ ``` All the available props on `BaseToast`, `SuccessToast`, `ErrorToast` or `InfoToast` components can be found here: [BaseToastProps](../src/types/index.ts#L86-L103). + +## Custom toast types with TypeScript + +When you create a custom toast type as in the example above, you can make it fully type-safe by utilizing Typescript declaration merging. + +Example: + +```tsx +import Toast, { ToastConfig } from 'react-native-toast-message'; + +interface TomatoToastParams { + uuid: string; + // ... +} + +declare module 'react-native-toast-message' { + export interface CustomToastParamTypes { + tomatoToast: TomatoToastParams; + } +} +``` + +Now the `props` parameter will be correctly typed according to the toast type. + +```tsx +const toastConfig: ToastConfig = { + tomatoToast: ({ text1, props }) => ( + + {text1} + {/* `props` will be of type `TomatoToastParams` here */} + {props.uuid} + + ) +}; +``` +Note that if you specify a custom toast type, then the config prop on the Toast component will become required: + +```tsx +export function App(props) { + return ( + <> + {...} + {/* Property 'config' is missing in type '{}' but required in type '{ config: ToastConfig; }'.ts(2741) */} + + ); +} +``` + +Then you can use the new toast type with the correct typing: +```ts +Toast.show({ + type: 'tomatoToast', + // if you mess something up in the props, typescript will scream at you properly + props: { uuid: 'bba1a7d0-6ab2-4a0a-a76e-ebbe05ae6d70' } +}); +``` diff --git a/src/Toast.tsx b/src/Toast.tsx index 2cc85c7c..2d65eebc 100644 --- a/src/Toast.tsx +++ b/src/Toast.tsx @@ -6,7 +6,8 @@ import { ToastHideParams, ToastProps, ToastRef, - ToastShowParams + ToastShowParams, + ToastType } from './types'; import { useToast } from './useToast'; @@ -121,7 +122,7 @@ function getRef() { return activeRef.current; } -Toast.show = (params: ToastShowParams) => { +Toast.show = (params: ToastShowParams) => { getRef()?.show(params); }; diff --git a/src/types/index.ts b/src/types/index.ts index 43cbda27..2809e198 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,15 +10,14 @@ import { export type ReactChildren = React.ReactNode; -export type ToastType = 'success' | 'error' | 'info' | (string & {}); export type ToastPosition = 'top' | 'bottom'; -export type ToastOptions = { +export type ToastOptions = { /** * Toast type. * Default value: `success` */ - type?: ToastType; + type?: T; /** * Style for the header text in the Toast (text1). */ @@ -85,20 +84,41 @@ export type ToastOptions = { * Called on Toast press */ onPress?: () => void; - /** - * Any custom props passed to the specified Toast type. - * Has effect only when there is a custom Toast type (configured via the `config` prop - * on the Toast instance) that uses the `props` parameter - */ - props?: any; -}; +} & (keyof CustomToastParamTypes extends never + ? { props?: any } + : T extends keyof CustomToastParamTypes + ? Required[T] extends never + ? { props?: any } + : undefined extends CustomToastParamTypes[T] + ? { + /** + * Any custom props passed to the specified Toast type. + * Has effect only when there is a custom Toast type (configured via the `config` prop + * on the Toast instance) that uses the `props` parameter + */ + props?: CustomToastParamTypes[T]; + } + : { + /** + * Any custom props passed to the specified Toast type. + * Has effect only when there is a custom Toast type (configured via the `config` prop + * on the Toast instance) that uses the `props` parameter + */ + props: CustomToastParamTypes[T]; + } + : { props?: any }); export type ToastData = { text1?: string; text2?: string; }; -export type ToastShowParams = ToastData & ToastOptions; +export type ToastShow = ( + params: ToastShowParams +) => void; + +export type ToastShowParams = ToastData & + ToastOptions; export type ToastHideParams = void; @@ -135,12 +155,33 @@ export type ToastConfigParams = { props: Props; }; +/** + * This interface is here to allow for types customization via declaration merging when using custom toast types. + * See [the docs](https://github.com/calintamas/react-native-toast-message/blob/main/docs/custom-layouts.md) for usage examples. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CustomToastParamTypes {} + +export interface BuiltinToastParamsTypes { + success?: never; + error?: never; + info?: never; +} + +export type ToastType = keyof (BuiltinToastParamsTypes & CustomToastParamTypes); + export type ToastConfig = { - [key: string]: (params: ToastConfigParams) => React.ReactNode; -}; + [key in keyof BuiltinToastParamsTypes]?: ( + params: ToastConfigParams + ) => React.ReactNode; +} & { + [key in keyof CustomToastParamTypes]-?: ( + params: ToastConfigParams + ) => React.ReactNode; +} & Record) => React.ReactNode>; export type ToastRef = { - show: (params: ToastShowParams) => void; + show: ToastShow; hide: (params: ToastHideParams) => void; }; @@ -148,11 +189,19 @@ export type ToastRef = { * `props` that can be set on the Toast instance. * They act as defaults for all Toasts that are shown. */ -export type ToastProps = { - /** - * Layout configuration for custom Toast types - */ - config?: ToastConfig; +export type ToastProps = (keyof CustomToastParamTypes extends never + ? { + /** + * Layout configuration for custom Toast types + */ + config?: ToastConfig; + } + : { + /** + * Layout configuration for custom Toast types + */ + config: ToastConfig; + }) & { /** * Toast type. * Default value: `success`