From 465c832ccf3e687181d27e9df43961995276d5ad Mon Sep 17 00:00:00 2001 From: Mafciejewicz Date: Wed, 20 Aug 2025 19:38:06 +0200 Subject: [PATCH 1/4] Add possibility to typecheck the custom toast type params --- src/types/index.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index 43cbda27..55e60994 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,7 +10,6 @@ import { export type ReactChildren = React.ReactNode; -export type ToastType = 'success' | 'error' | 'info' | (string & {}); export type ToastPosition = 'top' | 'bottom'; export type ToastOptions = { @@ -135,8 +134,25 @@ 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 {} + +interface ToastParamsTypes extends CustomToastParamTypes { + success?: never; + error?: never; + info?: never; +} + +export type ToastType = keyof ToastParamsTypes; + export type ToastConfig = { - [key: string]: (params: ToastConfigParams) => React.ReactNode; + [key in keyof ToastParamsTypes]: ( + params: ToastConfigParams + ) => React.ReactNode; }; export type ToastRef = { @@ -148,11 +164,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` From 0ac825573173f11256ec2aec47b470c00687c390 Mon Sep 17 00:00:00 2001 From: Mafciejewicz Date: Wed, 20 Aug 2025 19:38:16 +0200 Subject: [PATCH 2/4] Add docs for the feature --- docs/custom-layouts.md | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) 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' } +}); +``` From 09435eb45b6eb5b69aaa79caa90816d253197c49 Mon Sep 17 00:00:00 2001 From: Mafciejewicz Date: Wed, 20 Aug 2025 20:10:23 +0200 Subject: [PATCH 3/4] Make changes backwards-compatible and add `show` method typing --- src/Toast.tsx | 5 +++-- src/types/index.ts | 36 +++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 15 deletions(-) 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 55e60994..a0f9cb8b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,12 +12,12 @@ export type ReactChildren = React.ReactNode; 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). */ @@ -84,20 +84,28 @@ 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 + // eslint-disable-next-line @typescript-eslint/ban-types + ? { + props?: any, + } + : { + /** + * 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: ToastParamsTypes[T]; + }); 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; @@ -141,7 +149,7 @@ export type ToastConfigParams = { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomToastParamTypes {} -interface ToastParamsTypes extends CustomToastParamTypes { +export interface ToastParamsTypes extends CustomToastParamTypes { success?: never; error?: never; info?: never; @@ -153,10 +161,12 @@ export type ToastConfig = { [key in keyof ToastParamsTypes]: ( params: ToastConfigParams ) => React.ReactNode; -}; +} & Record + ) => React.ReactNode> export type ToastRef = { - show: (params: ToastShowParams) => void; + show: ToastShow; hide: (params: ToastHideParams) => void; }; From 9bc14b73d4eea365f228601cd8da434a4a993adf Mon Sep 17 00:00:00 2001 From: Mafciejewicz Date: Thu, 21 Aug 2025 16:33:15 +0200 Subject: [PATCH 4/4] Make the props optional if not specified in the type --- src/types/index.ts | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index a0f9cb8b..2809e198 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -85,27 +85,40 @@ export type ToastOptions = { */ onPress?: () => void; } & (keyof CustomToastParamTypes extends never - // eslint-disable-next-line @typescript-eslint/ban-types + ? { props?: any } + : T extends keyof CustomToastParamTypes + ? Required[T] extends never + ? { props?: any } + : undefined extends CustomToastParamTypes[T] ? { - props?: any, - } + /** + * 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: ToastParamsTypes[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 ToastShow = (params: ToastShowParams) => void; +export type ToastShow = ( + params: ToastShowParams +) => void; -export type ToastShowParams = ToastData & ToastOptions; +export type ToastShowParams = ToastData & + ToastOptions; export type ToastHideParams = void; @@ -149,21 +162,23 @@ export type ToastConfigParams = { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface CustomToastParamTypes {} -export interface ToastParamsTypes extends CustomToastParamTypes { +export interface BuiltinToastParamsTypes { success?: never; error?: never; info?: never; } -export type ToastType = keyof ToastParamsTypes; +export type ToastType = keyof (BuiltinToastParamsTypes & CustomToastParamTypes); export type ToastConfig = { - [key in keyof ToastParamsTypes]: ( - params: ToastConfigParams + [key in keyof BuiltinToastParamsTypes]?: ( + params: ToastConfigParams + ) => React.ReactNode; +} & { + [key in keyof CustomToastParamTypes]-?: ( + params: ToastConfigParams ) => React.ReactNode; -} & Record - ) => React.ReactNode> +} & Record) => React.ReactNode>; export type ToastRef = { show: ToastShow;