Skip to content

Instantly share code, notes, and snippets.

@nachodd
Last active July 6, 2023 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nachodd/008396bf4da251a357ea429c95ef8ec5 to your computer and use it in GitHub Desktop.
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
// 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