Subscribe on changes!

Valid typescript when using defineEmits() results in bad code transformation during compile

avatar
Feb 9th 2022

Version

3.2.30

Reproduction link

sfc.vuejs.org/

Steps to reproduce

In a <script setup lang="ts"> block, this causes an error during compilation:

<script setup lang="ts">
import { defineEmits } from 'vue'

interface Emits {
  (e: 'foo'): void
}
 
const emit: Emits = defineEmits(['foo'])
</script>

From vite:

  Expected "}" but found ":"
  9  |  const _sfc_main = /*#__PURE__*/_defineComponent({
  10 |    emits: ['foo'],
  11 |    setup(__props, { expose, emit: emit: Emits }) {
     |                                       ^
  12 |    expose();
  13 |  
  
      at failureErrorWithLog (C:\Work\scolasta2\node_modules\.pnpm\esbuild@0.13.15\node_modules\esbuild\lib\main.js:1493:15)
      at C:\Work\scolasta2\node_modules\.pnpm\esbuild@0.13.15\node_modules\esbuild\lib\main.js:1282:29
      at C:\Work\scolasta2\node_modules\.pnpm\esbuild@0.13.15\node_modules\esbuild\lib\main.js:629:9
      at handleIncomingPacket (C:\Work\scolasta2\node_modules\.pnpm\esbuild@0.13.15\node_modules\esbuild\lib\main.js:726:9)
      at Socket.readFromStdout (C:\Work\scolasta2\node_modules\.pnpm\esbuild@0.13.15\node_modules\esbuild\lib\main.js:596:7)
      at Socket.emit (node:events:390:28)
      at addChunk (node:internal/streams/readable:315:12)
      at readableAddChunk (node:internal/streams/readable:289:9)
      at Socket.Readable.push (node:internal/streams/readable:228:10)
      at Pipe.onStreamRead (node:internal/stream_base_commons:199:23)

What is expected?

It should compile

What is actually happening?

Doesn't compile


This looks like a transformation error in the vue compiler, since the following works fine as a workaround:

const emit = defineEmits(['foo']) as Emits

But the above code is valid typescript and shouldn't result in what is being generated.

avatar
Feb 10th 2022
const emit = defineEmits(['foo']) as Emits

Ah, the above doesn't work either, it results in a runtime error, "ReferenceError: defineEmits is not defined"

I can leave it untyped as a workaround, but since in my actual implementation the Emits interface is NOT contained in the SFC file, I need a way to type check the usage of the emit function.

EDIT: The following suboptimal method is my current workaround that seems to work:

const rawEmit = defineEmits(['foo'])
const emit: Emits = rawEmit
avatar
Feb 10th 2022

you should use type declaration

const emit = defineEmits<Emits>()
avatar
Feb 10th 2022

you should use type declaration

const emit = defineEmits<Emits>()

Ideally, but

in My actual implementation the Emits interface is NOT contained in the SFC file

avatar
Feb 10th 2022

I hate using GitHub on mobile... Didn't mean to close

avatar
Feb 20th 2022

I have this too in version 3.2.31

No interface available. I do not agree that it is a "feature" or a "nice to have"

avatar
Feb 20th 2022

@nborko how could you work around the problem with const emit: Emits = rawEmit

as that still needs the Emits type which does not exist... Thanks in advance.

avatar
Feb 21st 2022

@nborko how could you work around the problem with const emit: Emits = rawEmit

as that still needs the Emits type which does not exist... Thanks in advance.

You're looking for this?https://github.com/vuejs/core/issues/5393#issuecomment-1034335331

avatar
Feb 21st 2022

As a quick explanation of my setup, I have a subdirectory for each component with the following structure:

// types.ts

// heavily documented with tsdoc
interface MyComponentProps {
    ...
}

// heavily documented with tsdoc
interface MyComponentEmits {
    ...
}

// synthetic type for api-extractor
// tsdoc style documentation for the actual component
interface MyComponent {
    props: MyComponentProps
    emits: MyComponentEmits
}
<comment><!-- MyComponent.vue --></comment>

<script setup lang="ts">
import { MyComponentEmits, MyComponentProps } from './types'

const props: MyComponentProps = defineProps({
    ...
})

// This is broken
const emit: MyComponentEmits = defineEmits([ ... ])

// some functionality
</script>
// index.ts

export { default as MyComponent } from './MyComponent.vue'

This helps keep the documentation and types separated from the SFC to keep the code clean and easier to maintain, and I point api-extractor to a top level types.ts that imports and re-exports the types from each of the components, for further processing into API documentation.

EDIT: Updated the sample synthetic interface for clarity