Skip to content

feat: handle nested defineEmits#13262

Open
ST-DDT wants to merge 3 commits intovuejs:minorfrom
ST-DDT:feat/nested-defineEmits
Open

feat: handle nested defineEmits#13262
ST-DDT wants to merge 3 commits intovuejs:minorfrom
ST-DDT:feat/nested-defineEmits

Conversation

@ST-DDT
Copy link
Copy Markdown

@ST-DDT ST-DDT commented Apr 30, 2025

I'm not sure whether this is a fix or a feature.

This PR allows calling defineEmits as an argument of another method.

const transformed = transform(defineEmits(['foo', 'bar']));

My Usecase

const { foo, bar } = splitEmitFunctions(defineEmits(['foo', 'bar']));

foo(); // equivalent to emit('foo') including correct argument typing
splitEmitFunctions definition
import { getCurrentInstance } from 'vue';

type EmitterOverloads<T extends (event: string, ...args: unknown[]) => void> =
  T extends {
    (event: infer N0, ...args: infer A0): void;
    (event: infer N1, ...args: infer A1): void;
    (event: infer N2, ...args: infer A2): void;
    (event: infer N3, ...args: infer A3): void;
    (event: infer N4, ...args: infer A4): void;
    (event: infer N5, ...args: infer A5): void;
    (event: infer N6, ...args: infer A6): void;
    (event: infer N7, ...args: infer A7): void;
    (event: infer N8, ...args: infer A8): void;
    (event: infer N9, ...args: infer A9): void;
  }
    ? [
        [N0, (...args: A0) => void],
        [N1, N0 extends N1 ? never : (...args: A1) => void],
        [N2, N1 extends N2 ? never : (...args: A2) => void],
        [N3, N2 extends N3 ? never : (...args: A3) => void],
        [N4, N3 extends N4 ? never : (...args: A4) => void],
        [N5, N4 extends N5 ? never : (...args: A5) => void],
        [N6, N5 extends N6 ? never : (...args: A6) => void],
        [N7, N6 extends N7 ? never : (...args: A7) => void],
        [N8, N7 extends N8 ? never : (...args: A8) => void],
        [N9, N8 extends N9 ? never : (...args: A9) => void],
      ]
    : never;

type TupleToRecord<T extends ReadonlyArray<[string, unknown]>> = {
  [K in T[number] as K[0]]: K[1];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function splitEmitFunctions<T extends (...args: any) => void>(
  emit: T
): TupleToRecord<EmitterOverloads<T>> {
  const instance = getCurrentInstance();
  if (!instance) {
    throw new Error('splitEmitFunctions must be called within setup()');
  }

  return new Proxy(
    {},
    {
      get(_, key: string) {
        return (...args: unknown[]) => emit(key, ...args);
      },
    }
  ) as TupleToRecord<EmitterOverloads<T>>;
}

Without this patch, you have to use the following code:

const emit = defineEmits(['foo', 'bar']); // This must be on a separate line
const { foo, bar } = splitEmitFunctions(emit); 

foo();

poluting the scope with the emit variable, that shouldn't be used after this.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 30, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b75a14bb-f319-4ed4-aa9d-d421b5d4b848

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@ST-DDT ST-DDT marked this pull request as ready for review April 30, 2025 11:19
@Justineo
Copy link
Copy Markdown
Member

Can you provide more context? What was emit not able to achieve at the moment? Calling emit directly looks more explicit and easier to understand to me.

@ST-DDT
Copy link
Copy Markdown
Author

ST-DDT commented Apr 30, 2025

@Justineo My request is not about emit but defineEmits.
Basically, I want defineEmits() to behave like an actual function call (or more specifically the transpiled property __emit) and not like a special thing that only works when used with const emit = defineEmits().

Neglible: Also I don't like the empty assignment in the compiled code const emit = __emit; especially since I don't want emit variable in the first place.


As for splitEmitFunctions:

I prefer individual event functions to keep the usage symetrical between the publishing side and the consumer side:

ComponentA:

const { onValidChanged } = splitEmitFunctions(defineEmits<{ onValidChanged: [valid: boolean] }>());
// const emit = defineEmits<{ onValidChanged: [valid: boolean] }>();
// const onValidChanged: (valid : boolean) => void = (value) => emit('onValidChanged ', value);


watch(valid, (value) -> onValidChanged(value))

ComponentB:

<script setup lang="ts">
const onValidChanged: (valid: boolean) => void = (value) => continue.disabled = value;
</script>

<template>
  <ComponentA @onValidChanged="onValidChanged" />
</template>

So that: ComponentA's onValidChanged has the same type as ComponentB's onValidChanged:

ComponentA:

const { onValidChanged } = splitEmitFunctions(defineEmits<{ onValidChanged: OnValueChanged }>());

ComponentB:

<script setup lang="ts">
const onValidChanged: OnValueChanged = (value) => continue.disabled = value;
</script>

But you could also use it for different usecases such as simplified debug logging:

function logToConsole<T>(emit: T): T {
  return (...args) => {
    console.log("Event", ...args);
    emit(...args);
  }
}
const emit  = logToConsole(defineEmits<{ onValidChanged: [valid: boolean] }>()); 

watch(valid, (value) -> emit('onValidChanged ', value))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants