diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2026-01-09 12:21:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-09 12:21:08 +0900 |
| commit | 2a14025c29081bd8080b4dec0823a7625a791950 (patch) | |
| tree | e02f7fe6363457996efba020ec964bb6e4f041ed /packages/frontend/src/os.ts | |
| parent | Bump version to 2026.1.0-alpha.3 (diff) | |
| download | misskey-2a14025c29081bd8080b4dec0823a7625a791950.tar.gz misskey-2a14025c29081bd8080b4dec0823a7625a791950.tar.bz2 misskey-2a14025c29081bd8080b4dec0823a7625a791950.zip | |
fix(frontend): popupのemit型が正しく利用できるように修正 (#16826)
* fix(frontend): popupのemit型が正しく利用できるように修正
* fix: revert unnecessary code (for testing purpose)
* fix lint
* fix type errors
* fix types
* add comment
* fix
* fix
* fix: OverloadToUnionの仕組みを変更
* add comments, clean up
* fix lint
* fix types
* clean up [ci skip]
* fix
* add comments [ci skip]
Diffstat (limited to 'packages/frontend/src/os.ts')
| -rw-r--r-- | packages/frontend/src/os.ts | 124 |
1 files changed, 54 insertions, 70 deletions
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 3fb204c2b2..73f18bc6b5 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -8,13 +8,15 @@ import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { EventEmitter } from 'eventemitter3'; import * as Misskey from 'misskey-js'; -import type { Component, Ref } from 'vue'; +import type { Component, MaybeRef } from 'vue'; import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; import type { UploaderFeatures } from '@/composables/use-uploader.js'; import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue'; +import type { MkDialogReturnType } from '@/components/MkDialog.vue'; +import type { OverloadToUnion } from '@/types/overload-to-union.js'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -159,12 +161,34 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number { } // props に ref を許可するようにする -type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> }; +type PropsWithRefs<P> = { [K in keyof P]: MaybeRef<P[K]> }; +type ComponentProps<T extends Component> = PropsWithRefs<CP<T>>; +// 関数の引数が any[] (もっとも広義なもの) かどうかを判定し、any[] の場合は排除 (never) するヘルパー +type FilterSpecificFunc<T> = T extends (...args: any[]) => void + ? (any[] extends Parameters<T> ? never : T) + : T; + +// オブジェクトの各プロパティに対して再帰的、あるいは単純に適用する型関数 +type CleanFunctions<T> = { + [K in keyof T]: T[K] extends (...args: any[]) => any + ? FilterSpecificFunc<T[K]> + : T[K]; +}; + +// emitの関数群をオブジェクト型に変換する(InstanceType<Component>['$emit']はFunctionalComponent = ジェネリックコンポーネントでは使用できない) +type ComponentEmitsObject<C extends Component, IE = OverloadToUnion<ComponentEmit<C>>> = CleanFunctions<{ + [K in IE extends (evName: infer U, ...args: any[]) => any ? U & PropertyKey : never]: IE extends (evName: K, ...args: infer A) => infer R + ? (...args: A) => R + : (...args: any[]) => void; +}>; + +// NOTE: ジェネリック型つきのコンポーネントでは、emitsの型推論がうまく働かない(型変数を取り出すことはできないため) +// NOTE: emitsがOverloadToUnionで対応しているオーバーロードの数を超える場合は、OverloadToUnionの個数を増やせばOK export function popup<T extends Component>( component: T, props: ComponentProps<T>, - events: Partial<ComponentEmit<T>> = {}, + events: Partial<ComponentEmitsObject<T>> = {}, ): { dispose: () => void } { markRaw(component); @@ -192,10 +216,10 @@ export function popup<T extends Component>( export async function popupAsyncWithDialog<T extends Component>( componentFetching: Promise<T>, props: ComponentProps<T>, - events: Partial<ComponentEmit<T>> = {}, + events: Partial<ComponentEmitsObject<T>> = {}, ): Promise<{ dispose: () => void }> { let component: T; - let closeWaiting = () => {}; + let closeWaiting = () => { }; const timer = window.setTimeout(() => { closeWaiting = waiting(); @@ -291,23 +315,19 @@ export function confirm(props: { }); } -// TODO: const T extends ... にしたい -// https://zenn.dev/general_link/articles/813e47b7a0eef7#const-type-parameters -export function actions<T extends { +type ActionsAction = { value: string; text: string; - primary?: boolean, - danger?: boolean, -}[]>(props: { + primary?: boolean; + danger?: boolean; +}; + +export function actions<const T extends ActionsAction[]>(props: { type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; title?: string; text?: string; actions: T; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: T[number]['value']; -}> { +}): Promise<MkDialogReturnType<T[number]['value']>> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { ...props, @@ -321,7 +341,7 @@ export function actions<T extends { })), }, { done: result => { - resolve(result ? result : { canceled: true }); + resolve(result as MkDialogReturnType<T[number]['value']>); }, closed: () => dispose(), }); @@ -338,11 +358,7 @@ export function inputText(props: { default: string; minLength?: number; maxLength?: number; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: string; -}>; +}): Promise<MkDialogReturnType<string>>; // min lengthが指定されてたら result は null になり得ないことを保証する overload function export function inputText(props: { type?: 'text' | 'email' | 'password' | 'url'; @@ -353,11 +369,7 @@ export function inputText(props: { default?: string; minLength: number; maxLength?: number; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: string; -}>; +}): Promise<MkDialogReturnType<string>>; export function inputText(props: { type?: 'text' | 'email' | 'password' | 'url'; title?: string; @@ -367,11 +379,7 @@ export function inputText(props: { default?: string | null; minLength?: number; maxLength?: number; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: string | null; -}>; +}): Promise<MkDialogReturnType<string | null>>; export function inputText(props: { type?: 'text' | 'email' | 'password' | 'url'; title?: string; @@ -381,11 +389,7 @@ export function inputText(props: { default?: string | null; minLength?: number; maxLength?: number; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: string | null; -}> { +}): Promise<MkDialogReturnType<string | null>> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { title: props.title, @@ -400,7 +404,7 @@ export function inputText(props: { }, }, { done: result => { - resolve(result ? result : { canceled: true }); + resolve(result as MkDialogReturnType<string | null>); }, closed: () => dispose(), }); @@ -414,33 +418,21 @@ export function inputNumber(props: { placeholder?: string | null; autocomplete?: string; default: number; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: number; -}>; +}): Promise<MkDialogReturnType<number>>; export function inputNumber(props: { title?: string; text?: string; placeholder?: string | null; autocomplete?: string; default?: number | null; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: number | null; -}>; +}): Promise<MkDialogReturnType<number | null>>; export function inputNumber(props: { title?: string; text?: string; placeholder?: string | null; autocomplete?: string; default?: number | null; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: number | null; -}> { +}): Promise<MkDialogReturnType<number | null>> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { title: props.title, @@ -453,7 +445,7 @@ export function inputNumber(props: { }, }, { done: result => { - resolve(result ? result : { canceled: true }); + resolve(result as MkDialogReturnType<number | null>); }, closed: () => dispose(), }); @@ -465,11 +457,7 @@ export function inputDatetime(props: { text?: string; placeholder?: string | null; default?: string | null; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: Date; -}> { +}): Promise<MkDialogReturnType<Date>> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { title: props.title, @@ -481,7 +469,7 @@ export function inputDatetime(props: { }, }, { done: result => { - resolve(result != null && result.result != null ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true }); + resolve(result != null && typeof result.result === 'string' ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true }); }, closed: () => dispose(), }); @@ -508,11 +496,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props: text?: string; default?: D; items: (MkSelectItem<C> | undefined)[]; -}): Promise<{ - canceled: true; result: undefined; -} | { - canceled: false; result: Exclude<D, undefined> extends null ? C | null : C; -}> { +}): Promise<MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>> { return new Promise(resolve => { const { dispose } = popup(MkDialog, { title: props.title, @@ -523,7 +507,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props: }, }, { done: result => { - resolve(result ? result : { canceled: true }); + resolve(result as MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>); }, closed: () => dispose(), }); @@ -582,7 +566,7 @@ export function form<F extends Form>(title: string, f: F): Promise<{ canceled: t return new Promise(resolve => { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, { done: result => { - resolve(result); + resolve(result as { canceled?: false, result: GetFormResultType<F> }); }, closed: () => dispose(), }); @@ -634,16 +618,16 @@ export async function pickEmoji(anchorElement: HTMLElement, opts: ComponentProps }); } -export async function cropImageFile(imageFile: File | Blob, options: { +export async function cropImageFile<F extends File | Blob>(imageFile: F, options: { aspectRatio: number | null; -}): Promise<File> { +}): Promise<F> { return new Promise(resolve => { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { imageFile: imageFile, aspectRatio: options.aspectRatio, }, { ok: x => { - resolve(x); + resolve(x as F); }, closed: () => dispose(), }); |