-
-
Save nachodd/008396bf4da251a357ea429c95ef8ec5 to your computer and use it in GitHub Desktop.
JSDoc type annotation example - returning different types depending on whether it is defined or not - Toast composable example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: this was a test to determine if useToast function type could be annotated using JSDoc. | |
// The catch is that thi function returns a different type depending if a parameter is present or not | |
// For some reason, it is not recognizing @param {undefined}, so that the type can not be inffered correctly | |
// Typescript solution: | |
// https://www.typescriptlang.org/play?#code/C4TwDgpgBMD2CGBnYB5ARgKwgY2FAvFAN4BQUUy8wArogPwBcFwATgJYB2A5gNxlRtssDo2btufcsDbAANhCbJxvfgBMIibOzDThi1pxXl4uNsPpMAgixbwQAHngcQAPj4BfPiVCQYCZABiHARQABRwSMBMEcjoWLgAlAQuUABusGyqXj7QMcAAwsKssLKIIaRS-sCIAKIAHmBO6qqiaLAlEE6SUGAssAC2Ovny8CyWsrKWpqkQre0jHN3YI2MToqFJ+Cnpmd2IEMAACn2DBSvjsuub2xlZ-HnDggDWV8lpt91wXFzy9Y0czVeW3eu34ABI8tEqkEPNlwNBaBAACpVFA6MwcMqECpQIQcYqlOYdLokdwkEgAeip1JptLpdPJVKgqlgGg4AHI8P0qMAICwoAAzWD84AACzYZQgdXgg3kJHkeDytQaTQgqhCAvgpQg8oOPROQ3OEym0hmIVY1B1eOQuKNshCGzeRDJ1rw+yOBrOnVW9sIjuBzpIrr8kUe2CeDuuxBd5kVsG+vxVALVkadZKDsagEKqUMiQQdkJDsUwOGAUZxTJZbM5UG5wF5-KFIvFkulsp15GtHQAdLJ46EAER5OKlgcAGiLZdJjPps7nFJnUAAqocACKWJE1VfkgXUDimYRQREoyL2NG6TFQKW8gFlY+o9HmKAAHyPyYFnBThD36g-HDVLihLAj6YqI54YogCRMOBT7XhAt5vr+n7qnQk75rmyCFPifSlMQ-BsAKYTARe9DdniBKQXh5DkCwBzUCwwQ4tRk7Kv8zRjvwzG9AMhrehcJpsDMHHMeQ2aRMJInLHxEwScx7rHDxXqjBcsnUQ8sjPKplQJhAfyqqoWmCu0TAAIxQEyeJXvaiBUBKH4puovK4EgV7INAYCwIgbBoPIUBoNQXATmwXDUFqgpsBgVDQJxUDuBQtmIPZZQPEUOFlC5TggOZFLLmuG5bvwZLkBA2pUdRtE0AxWZ5FAGXOIV04ZpiipYke+wnsgGyLhwsB4JwAp8soMCirk8JQLAhFitAowhf08FRFAQ6oiWuADmEWqILAw0AO6wNthi1cEg3Cgk3iIKERDSHIChQOy7LuKdlI5f5fWEQAkrVqjqgABi5eRBE1NrVAATCE96RBd5FpdELCWg9UBPTAY0TcN00sLN80CGU-WDSmQg2KWsggGdwNkXa5JPfOVPU9Ti4bbAE6Hl53C+RV9HBDkWMCBwA02AdU1I74+O0bgRMMDue4HsE4PIAAyoY8gAEp0QxQEgRYbXIg+JFJBhwD5kxUBs1VYnII1wbVAAzGD7VVPLLMQMrlUcF1TKcyjAszdQc34lzOME+qwuE8TVsXQ9QA | |
// I was able to (almost) recreate the above type definition using JSDoc, but the type is not inferred correctly when undefined (or absent parameter) | |
// is passed to useToast | |
import { ref } from 'vue' | |
const toasts = ref([]) | |
const nextToastId = ref(1) | |
const toastsExpanded = ref(false) | |
const promptClearAllActive = ref(false) | |
const MAX_ITEMS = 18 | |
const DEFAULT_DURATION = 3000 | |
let toastTimeout = null | |
let toastRouteChangeTimeout = null | |
function avoidDuplicates(toast) { | |
let result = false | |
if (typeof toast.avoidDuplicates === 'boolean' && toast.avoidDuplicates) { | |
result = isToastAlreadyDisplayed(toast) | |
} | |
else if (typeof toast.avoidDuplicates === 'object') { | |
// if a criteria object is provided, check that no existing toast matches all properties | |
const lookupToastByCriteria = toasts.value.findIndex(t => { | |
return Object.keys(toast.avoidDuplicates).every(key => { | |
return t[key] === toast.avoidDuplicates[key] | |
}) | |
}) | |
if (lookupToastByCriteria !== -1) { | |
result = true | |
// update existing | |
Object.assign(toasts.value[lookupToastByCriteria], toast) | |
setupExpiryForToast(toasts.value[lookupToastByCriteria]) | |
} | |
} | |
return result | |
} | |
function isToastAlreadyDisplayed(toast) { | |
return Boolean(toasts.value.find(displayedToast => | |
toast.description === displayedToast.description && | |
toast.title === displayedToast.title && | |
toast.status === displayedToast.status | |
)) | |
} | |
function removeToast(toast) { | |
const toastIndex = toasts.value.findIndex(t => t.id === toast.id) | |
// Check if we found it, maybe the user closed it manually | |
if (toastIndex !== -1) { | |
toasts.value.splice(toastIndex, 1) | |
} | |
if (toasts.value.length < 2) { | |
if (toastsExpanded.value) { | |
toggleExpanded() | |
} | |
} | |
} | |
function routeChange() { | |
// Remove toasts on route change: | |
// - Only the ones that have no timeout | |
// - Not immediatly, just after DEFAULT_DURATION millisecons | |
const toastToBeDeleted = toasts.value | |
.filter(toast => { | |
let duration = toast.status !== 'success' ? 0 : DEFAULT_DURATION | |
if (toast.duration !== undefined) { | |
duration = toast.duration | |
} | |
return duration === 0 | |
}) | |
.map(toast => toast.id) | |
toastRouteChangeTimeout = setTimeout(() => { | |
toasts.value = toasts.value.filter(toast => !toastToBeDeleted.includes(toast.id)) | |
}, DEFAULT_DURATION) | |
} | |
/** | |
* @typedef toastObject | |
* @type {object} | |
* @property {string} [status='info'] status of the toast | |
* @property {string} [icon] icon of the toast | |
* @property {string} title title of the toast | |
* @property {string} description description of the toast | |
* @property {Array} [actions] array of actions | |
*/ | |
/** | |
* @typedef toastFn | |
* @type {(toast: toastObject) => void)} | |
* @param {toastObject} toast Object. 'title' and 'description' are mandatory | |
* @returns {void} | |
*/ | |
/** | |
* Triggers a "toast" message. | |
* @type {toastFn} | |
*/ | |
function $toast(toast) { | |
if (avoidDuplicates(toast)) return | |
let toastWithId = { id: nextToastId.value++, ...toast } | |
// Unshift + Pop => This is a queue of MAX_ITEMS elements | |
toasts.value.unshift(toastWithId) | |
while (toasts.value.length > MAX_ITEMS) { | |
toasts.value.pop() | |
} | |
setupExpiryForToast(toastWithId) | |
if (toasts.value.length < 2) { | |
if (toastsExpanded.value) { | |
toggleExpanded() | |
} | |
} | |
} | |
function setupExpiryForToast(toast) { | |
// Remove toast if duration unless duration is 0 or status in (error, warning) | |
let duration = toast.status !== 'success' ? 0 : DEFAULT_DURATION | |
if (toast.duration !== undefined) { | |
duration = toast.duration | |
} | |
if (duration > 0) { | |
setTimeout(() => removeToast(toast), duration) | |
} | |
} | |
function clearToasts() { | |
if (toastTimeout) { | |
clearTimeout(toastTimeout) | |
} | |
if (toastRouteChangeTimeout) { | |
clearTimeout(toastRouteChangeTimeout) | |
} | |
toasts.value = [] | |
} | |
function clearAll(event) { | |
while (toasts.value.length > 0) { | |
removeToast(toasts.value[0]) | |
} | |
promptClearAllActive.value = false | |
toastsExpanded.value = false | |
} | |
function toggleExpanded() { | |
toastsExpanded.value = !toastsExpanded.value | |
promptClearAllActive.value = false | |
} | |
function setPromptClearAll(value) { | |
promptClearAllActive.value = true | |
} | |
function toastClick(index) { | |
const hasToastsAndAreExapnded = (toasts.value.length > 1 || toastsExpanded.value) | |
const isFirstOrLast = index === 0 || index === toasts.value.length - 1 | |
if (hasToastsAndAreExapnded && isFirstOrLast) { | |
toggleExpanded() | |
} | |
} | |
/** | |
* @typedef toastControls | |
* @type {object} | |
* @property {Array} toasts array of toasts | |
* @property {boolean} toastsExpanded true if toasts are expanded | |
* @property {boolean} promptClearAllActive true if prompt clear all is active | |
* @property {function} removeToast remove a toast | |
* @property {function} clearToasts clear all toasts | |
* @property {function} routeChange remove toasts on route change | |
* @property {function} clearAll clear all toasts | |
* @property {function} setPromptClearAll set prompt clear all | |
* @property {function} toastClick handle toast click | |
* @property {toastFn} $toast trigger a toast | |
* @property {function} toasts array of toasts | |
* | |
*/ | |
/** | |
* @typedef UseToastOptions | |
* @type {object} | |
* @property {boolean} [controls=false] false if you want to return toastFn | |
* @property {boolean} [controls=true] true if you want to return toastControls | |
*/ | |
// This doesn't work. it doesn't recognize @param { undefined } | |
/* | |
* Returns the $toast function or an object with a bunch of properties | |
* @function | |
* @param { undefined } options - An object with a `controls` property that determines the return value. | |
`* @returns {$toast:toastFn} toastControls if options.controls is true, else $toast function | |
*/ | |
/* | |
* Returns the $toast function or an object with a bunch of properties | |
* @function | |
* @param {{ controls: true }} options - An object with a `controls` property that determines the return value. | |
`* @returns {toastControls} toastControls if options.controls is true, else $toast function | |
*/ | |
// This almost works, the output is similar to the TS alternative (see link at the top): | |
/* | |
* Returns the $toast function or an object with a bunch of properties | |
* @template {UseToastOptions} Options | |
* @extends {UseToastOptions|undefined = undefined} | |
* @param {Options|undefined} [options] | |
* @returns {Options extends undefined ? toastFn : toastControls} | |
*/ | |
// Same here: | |
/* | |
* @template {UseToastOptions | undefined} [Options=undefined] | |
* @param {UseToastOptions|undefined} [options=undefined] | |
* @returns {Options extends undefined ? toastFn : toastControls} | |
*/ | |
// This almost works, but it doesn't recognize the undefined option parameter | |
/* | |
* Returns the $toast function or an object with a bunch of properties | |
* @overload | |
* @param {Object} [options] - An object with a `controls` property that determines the return value. | |
* @returns {toastControls} toastControls if options.controls is true, else $toast function | |
** | |
* @overload | |
* @param {} [options] - An object with a `controls` property that determines the return value. | |
* @returns {toastFn} $toast function | |
*/ | |
// This do works, but function needs to be defined as const zzz = (..) => {} | |
// To make it work, @overload has to be used.. but we have the same issue as before (https://stackoverflow.com/a/75502394/965452) | |
/** | |
* @type {{ | |
* (options: {controls: true}) => toastControls; | |
* (options: undefined) => typeof $toast; | |
* }} | |
*/ | |
const useToast = (options = undefined) => { | |
if (options?.controls) { | |
return { | |
toasts, | |
toastsExpanded, | |
promptClearAllActive, | |
$toast, | |
removeToast, | |
clearToasts, | |
routeChange, | |
clearAll, | |
setPromptClearAll, | |
toastClick, | |
toggleExpanded | |
} | |
} | |
return $toast | |
} | |
let test = useToast({controls: true}) | |
test.$toast({}) // works, type inferred | |
let test2 = useToast({controls: false}}) | |
test2({}) // works, type inferred | |
let test3 = useToast() | |
test3({}) // works, type inferred | |
export default useToast | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment