typescriptlaravelvuejs3composable

declaration file for custom hook vue3 composable


I have a laravel, vue3 and typescript project.

in the users module, UsersGrid, I have a custom composable, useUser , I want to type it, but every way I try, an error always comes up

as a custom hook ( composable ) i cant npm install @types/XYZ

tsconfig in root :

{
  "compilerOptions": {
    // As long as you are using a build tool, we recommend you to author and ship in ES modules.
    // Even if you are targeting Node.js, because
    //  - `CommonJS` is too outdated
    //  - the ecosystem hasn't fully caught up with `Node16`/`NodeNext`
    // This recommendation includes environments like Vitest, Vite Config File, Vite SSR, etc.
    "module": "ESNext",

    // We expect users to use bundlers.
    // So here we enable some resolution features that are only available in bundlers.
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    // `allowImportingTsExtensions` can only be used when `noEmit` or `emitDeclarationOnly` is set.
    // But `noEmit` may cause problems with solution-style tsconfigs:
    // <https://github.com/microsoft/TypeScript/issues/49844>
    // And `emitDeclarationOnly` is not always wanted.
    // Considering it's not likely to be commonly used in Vue codebases, we don't enable it here.

    // Required in Vue projects
    "jsx": "preserve",
    "jsxImportSource": "vue",

    // `"noImplicitThis": true` is part of `strict`
    // Added again here in case some users decide to disable `strict`.
    // This enables stricter inference for data properties on `this`.
    "noImplicitThis": true,
    "strict": true,

    // <https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#verbatimmodulesyntax>
    // Any imports or exports without a type modifier are left around. This is important for `<script setup>`.
    // Anything that uses the type modifier is dropped entirely.
    "verbatimModuleSyntax": true,

    // A few notes:
    // - Vue 3 supports ES2016+
    // - For Vite, the actual compilation target is determined by the
    //   `build.target` option in the Vite config.
    //   So don't change the `target` field here. It has to be
    //   at least `ES2020` for dynamic `import()`s and `import.meta` to work correctly.
    // - If you are not using Vite, feel free to overwrite the `target` field.
    "target": "ESNext",
    // For spec compilance.
    // `true` by default if the `target` is `ES2020` or higher.
    // Explicitly set it to `true` here in case some users want to overwrite the `target`.
    "useDefineForClassFields": true,

    // Recommended
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    // See <https://github.com/vuejs/vue-cli/pull/5688>
    "skipLibCheck": true,
  }
}

UsersGrid :

<template>
    <div>
        <ag-grid-vue :style="gridStyle" suppressHorizontalScroll="true" :gridOptions="gridOptions" :class="gridMode"
            :columnDefs="columnDefs.value" :rowData="rowData" :loadingOverlayComponent="CardOverlay">
        </ag-grid-vue>
    </div>
</template> 

<script setup lang="ts">

import { ref, watch, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router';
import { AgGridVue } from "ag-grid-vue3";
import { useI18n } from 'vue-i18n'
import { gridOptions, gridStyle } from "../../helpers/gridSettings";
import ActionButtons from '../../components/grids/ActionButtons.vue'
import CardOverlay from '../../components/general/CardOverlay.vue'
import Tr from "../../helpers/translation";
import useUser from '../../composables/useUser' // Could not find a declaration file for module '../../composables/useUser'. 'c:/Users/kaue.zuquetto/Desktop/Dev/Portal/resources/js/composables/useUser.js' implicitly has an 'any' type.
import { useStore } from 'vuex'

import type { User, ColDef } from '../../../../types'



const { t, locale } = useI18n()
const store = useStore()
const router = useRouter()
const gridMode = ref('ag-theme-alpine')
const columnDefs = ref()
const {
    getAllData: getUsers,
    deleteData: deleteUser,
    rowData,
} = useUser();

const details = (id: number) => {
    router.push(Tr.i18nRoute({ name: 'user.read', params: { id } }))
}

const i18nGrid = () => {
    columnDefs.value = [
        [],
        { headerName: 'ID', field: 'id', flex: 1 },
        { headerName: t('user.grid.colName'), field: 'name', flex: 3 },
        { headerName: t('user.grid.colEmail'), field: 'email', flex: 3 },
        { headerName: t('grid.created_at'), field: 'created_at', flex: 3 },
        { headerName: t('grid.updated_at'), field: 'updated_at', flex: 3 },
        {
            headerName: '',
            flex: 1.5,
            sortable: false,
            filter: false,
            cellRenderer: ActionButtons,
            cellRendererParams: function () {
                return {
                    details: details,
                    delete: deleteUser,
                    id: arguments[0].data.id,
                    buttons: ['details', 'delete']
                }
            },
        },
    ] as ColDef<User>[];
}

const options = computed(() => {
    return store.getters["preferences/options"]
})

watch(options, () => {
    gridMode.value = store.state.preferences.options.grid
})

watch(locale, () => {
    i18nGrid()
})

onMounted(() => {
    i18nGrid()
    getUsers()
    gridMode.value = store.state.preferences.options.grid
})

</script>

my useUser composable :

import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import useSwal from '../helpers/swal'
import Tr from '../helpers/translation'
import { gridOptions, gridStyle } from "../helpers/gridSettings"

const { confirm_delete_swal, toast_success, toast_error } = useSwal()

const useUser = (userID = null) => {

    const router = useRouter()
    const user = ref({})
    const isLoading = ref(false)
    const processing = ref(false)
    const errors = ref(false)
    const rowData = ref(null)

    const getAllData = async () => {
        try {
            const response = await axios.get('/api/users')
            const { status } = response
            if (status === 200) {
                rowData.value = response.data.data
                rowData.value.sort((a, b) => b.id - a.id)
            } else {
                console.log('ERROR STATUS:  ' + response.status)
                toast_error()
            }
        } catch (e) {
            gridOptions.api.hideOverlay()
            gridOptions.api.showNoRowsOverlay()
            toast_error()
            console.log(e)
        }
    }

    const getData = async () => {
        try {
            isLoading.value = true
            const url = '/api/user/' + userID + '/read';
            const response = await axios.get(url)
            if (response.status === 200) {
                const { data } = response.data
                user.value = data
            } else {
                console.log('ERROR STATUS:  ' + response.status)
                toast_error()
            }
        } catch (e) {
            toast_error()
            console.log(e)
        } finally {
            isLoading.value = false
        }
    }

    const createData = async (formData) => {
        try {
            processing.value = true
            const response = await axios.post('/api/user/create', formData)
            if (response.status === 200) {
                const { data } = response.data
                errors.value = {}
                router.push(Tr.i18nRoute({ name: 'user.read', params: { id: data.id } }))
                toast_success()
            } else {
                toast_error()
                console.log('ERROR STATUS:  ' + response.status)
            }
        } catch (e) {
            toast_error()
            console.log(e)
            if (e.response && e.response.data.errors) {
                errors.value = e.response.data.errors
            }
        } finally {
            processing.value = false
        }
    }

    const updateData = async (formData, userRoles) => {
        try {
            processing.value = true
            const response = await axios.put('/api/user/' + userID + '/update', formData)
            if (response.status === 200) {
                const { data } = response.data
                errors.value = {}
                router.replace(Tr.i18nRoute({ name: 'user.read', params: { id: data } }))
                userRoles.assignRoles()
            } else {
                toast_error()
                console.log('ERROR STATUS:  ' + response.status)
            }
        } catch (e) {
            toast_error()
            console.log(e)
            toast_error()
            if (e.response && e.response.data.errors) {
                errors.value = e.response.data.errors
            }
        } finally {
            processing.value = false
        }
    }

    const deleteData = async (id) => {
        try {
            const { isConfirmed } = await confirm_delete_swal(id)
            if (isConfirmed) {
                const response = await axios.delete('/api/user/' + id + '/delete')
                const { status } = response
                if (status == 200) {
                    getAllData()
                    toast_success()
                } else {
                    toast_error()
                }
            }
        } catch (e) {
            toast_error()
            console.log(e)
            if (e.response && e.response.data.errors) {
                errors.value = e.response.data.errors
            }
        }
    }

    userID ? getData() : ''

    return {
        getAllData,
        rowData,
        getData,
        user,
        createData,
        updateData,
        deleteData,
        isLoading,
        processing,
        errors,
    }
}
export default useUser;

how can create a declaration file form my useUser hook ? its always complaining about not being a module, or not being callable

useUser.d.ts :

//Could not find a declaration file for module '../../composables/useUser'. implicitly has an 'any' type.ts(7016)

declare module 'useUser' {
    export function useUser(): unknown
  }

Solution

  • Unless otherwise configured, TypeScript files (or components) expect only typed imports. Your composable is a .js file. You can change this behavior and allow JS imports with the allowJs compiler option in tsconfig

    "compilerOptions": {
         "allowJs": true,
    }
    

    Alternatively, you can rewrite your composable in TypeScript.