From 3d39daff8cf45e7acac29c3ca0bfeadac7374eb2 Mon Sep 17 00:00:00 2001 From: Lhcfl <Lhcfl@outlook.com> Date: Wed, 3 Apr 2024 23:10:49 +0800 Subject: [PATCH] refactor: Add more type support to MkPagination --- .../client/src/components/MkChannelList.vue | 5 +- .../src/components/MkFileListForAdmin.vue | 6 +- packages/client/src/components/MkNotes.vue | 5 +- .../client/src/components/MkNotifications.vue | 30 +-- .../client/src/components/MkPagination.vue | 17 +- packages/client/src/components/MkTimeline.vue | 38 +++- .../client/src/pages/about.federation.vue | 3 +- packages/client/src/pages/admin/files.vue | 2 +- packages/client/src/pages/instance-info.vue | 2 +- packages/client/src/pages/note-history.vue | 7 +- packages/firefish-js/src/api.types.ts | 41 ++--- packages/firefish-js/src/consts.ts | 21 +++ packages/firefish-js/src/entities.ts | 172 ++++++++++-------- packages/firefish-js/src/index.ts | 3 + packages/firefish-js/src/streaming.types.ts | 14 +- packages/firefish-js/src/type-utils.ts | 13 ++ 16 files changed, 234 insertions(+), 145 deletions(-) create mode 100644 packages/firefish-js/src/type-utils.ts diff --git a/packages/client/src/components/MkChannelList.vue b/packages/client/src/components/MkChannelList.vue index d6c9a44c4e..15f199c90f 100644 --- a/packages/client/src/components/MkChannelList.vue +++ b/packages/client/src/components/MkChannelList.vue @@ -24,13 +24,14 @@ <script lang="ts" setup> import MkChannelPreview from "@/components/MkChannelPreview.vue"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { PagingOf } from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue"; import { i18n } from "@/i18n"; +import type { entities } from "firefish-js"; const props = withDefaults( defineProps<{ - pagination: Paging; + pagination: PagingOf<entities.Channel>; noGap?: boolean; extractor?: (item: any) => any; }>(), diff --git a/packages/client/src/components/MkFileListForAdmin.vue b/packages/client/src/components/MkFileListForAdmin.vue index 2889a74c81..d4cd8574aa 100644 --- a/packages/client/src/components/MkFileListForAdmin.vue +++ b/packages/client/src/components/MkFileListForAdmin.vue @@ -1,7 +1,7 @@ <template> <div> <MkPagination - v-slot="{ items }" + v-slot="{ items }: { items: entities.DriveFile[]}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }" @@ -53,13 +53,15 @@ <script lang="ts" setup> import { acct } from "firefish-js"; +import type { entities } from "firefish-js"; import MkPagination from "@/components/MkPagination.vue"; +import type { PagingOf } from "@/components/MkPagination.vue"; import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue"; import bytes from "@/filters/bytes"; import { i18n } from "@/i18n"; defineProps<{ - pagination: any; + pagination: PagingOf<entities.DriveFile>; viewMode: "grid" | "list"; }>(); </script> diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue index 07ed2c3f12..2351cd6e47 100644 --- a/packages/client/src/components/MkNotes.vue +++ b/packages/client/src/components/MkNotes.vue @@ -40,17 +40,18 @@ <script lang="ts" setup> import { ref } from "vue"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { PagingOf } from "@/components/MkPagination.vue"; import XNote from "@/components/MkNote.vue"; import XList from "@/components/MkDateSeparatedList.vue"; import MkPagination from "@/components/MkPagination.vue"; import { i18n } from "@/i18n"; import { scroll } from "@/scripts/scroll"; +import type { entities } from "firefish-js"; const tlEl = ref<HTMLElement>(); defineProps<{ - pagination: Paging; + pagination: PagingOf<entities.Note>; noGap?: boolean; disableAutoLoad?: boolean; }>(); diff --git a/packages/client/src/components/MkNotifications.vue b/packages/client/src/components/MkNotifications.vue index abb28d91b8..c8b54f4074 100644 --- a/packages/client/src/components/MkNotifications.vue +++ b/packages/client/src/components/MkNotifications.vue @@ -19,12 +19,8 @@ :no-gap="true" > <XNote - v-if=" - ['reply', 'quote', 'mention'].includes( - notification.type, - ) - " - :key="notification.id" + v-if="isNoteNotification(notification)" + :key="'nn-' + notification.id" :note="notification.note" :collapsed-reply=" notification.type === 'reply' || @@ -34,7 +30,7 @@ /> <XNotification v-else - :key="notification.id" + :key="'n-' + notification.id" :notification="notification" :with-time="true" :full="true" @@ -47,8 +43,7 @@ <script lang="ts" setup> import { computed, onMounted, onUnmounted, ref } from "vue"; -import type { notificationTypes } from "firefish-js"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { entities, notificationTypes } from "firefish-js"; import MkPagination from "@/components/MkPagination.vue"; import XNotification from "@/components/MkNotification.vue"; import XList from "@/components/MkDateSeparatedList.vue"; @@ -66,20 +61,29 @@ const stream = useStream(); const pagingComponent = ref<InstanceType<typeof MkPagination>>(); -const pagination: Paging = { +const pagination = { endpoint: "i/notifications" as const, limit: 10, params: computed(() => ({ includeTypes: props.includeTypes ?? undefined, - excludeTypes: props.includeTypes ? undefined : me.mutingNotificationTypes, + excludeTypes: props.includeTypes ? undefined : me?.mutingNotificationTypes, unreadOnly: props.unreadOnly, })), }; -const onNotification = (notification) => { +function isNoteNotification( + n: entities.Notification, +): n is + | entities.ReplyNotification + | entities.QuoteNotification + | entities.MentionNotification { + return n.type === "reply" || n.type === "quote" || n.type === "mention"; +} + +const onNotification = (notification: entities.Notification) => { const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) - : me.mutingNotificationTypes.includes(notification.type); + : me?.mutingNotificationTypes.includes(notification.type); if (isMuted || document.visibilityState === "visible") { stream.send("readNotification", { id: notification.id, diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue index f9a9e8a288..aee1fffbe3 100644 --- a/packages/client/src/components/MkPagination.vue +++ b/packages/client/src/components/MkPagination.vue @@ -66,10 +66,10 @@ </transition> </template> -<script lang="ts" setup> +<script lang="ts" setup generic="E extends keyof Endpoints"> import type { ComputedRef } from "vue"; import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue"; -import type { Endpoints } from "firefish-js"; +import type { Endpoints, TypeUtils } from "firefish-js"; import * as os from "@/os"; import { getScrollContainer, @@ -100,11 +100,13 @@ export interface Paging<E extends keyof Endpoints = keyof Endpoints> { offsetMode?: boolean; } +export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>; + const SECOND_FETCH_LIMIT = 30; const props = withDefaults( defineProps<{ - pagination: Paging; + pagination: Paging<E>; disableAutoLoad?: boolean; displayLimit?: number; }>(), @@ -113,14 +115,17 @@ const props = withDefaults( }, ); +const slots = defineSlots<{ + default(props: { items: Item[] }): unknown; + empty(props: null): never; +}>(); + const emit = defineEmits<{ (ev: "queue", count: number): void; (ev: "status", error: boolean): void; }>(); -type Item = Endpoints[typeof props.pagination.endpoint]["res"] & { - id: string; -}; +type Item = Endpoints[E]["res"][number]; const rootEl = ref<HTMLElement>(); const items = ref<Item[]>([]); diff --git a/packages/client/src/components/MkTimeline.vue b/packages/client/src/components/MkTimeline.vue index 216723d30e..456a481fa2 100644 --- a/packages/client/src/components/MkTimeline.vue +++ b/packages/client/src/components/MkTimeline.vue @@ -44,7 +44,7 @@ <script lang="ts" setup> import { computed, onUnmounted, provide, ref } from "vue"; -import type { Endpoints } from "firefish-js"; +import type { entities } from "firefish-js"; import MkPullToRefresh from "@/components/MkPullToRefresh.vue"; import XNotes from "@/components/MkNotes.vue"; import MkInfo from "@/components/MkInfo.vue"; @@ -54,10 +54,23 @@ import { isSignedIn, me } from "@/me"; import { i18n } from "@/i18n"; import { defaultStore } from "@/store"; import icon from "@/scripts/icon"; -import type { Paging } from "@/components/MkPagination.vue"; +import type { EndpointsOf } from "@/components/MkPagination.vue"; + +export type TimelineSource = + | "antenna" + | "home" + | "local" + | "recommended" + | "social" + | "global" + | "mentions" + | "directs" + | "list" + | "channel" + | "file"; const props = defineProps<{ - src: string; + src: TimelineSource; list?: string; antenna?: string; channel?: string; @@ -73,7 +86,7 @@ const emit = defineEmits<{ const tlComponent = ref<InstanceType<typeof XNotes>>(); const pullToRefreshComponent = ref<InstanceType<typeof MkPullToRefresh>>(); -let endpoint = ""; // keyof Endpoints +let endpoint: EndpointsOf<entities.Note[]>; // keyof Endpoints let query: { antennaId?: string | undefined; withReplies?: boolean; @@ -81,7 +94,9 @@ let query: { listId?: string | undefined; channelId?: string | undefined; fileId?: string | undefined; -}; +} = {}; + +// FIXME: The type defination is wrong here, need fix let connection: { on: ( arg0: string, @@ -96,14 +111,14 @@ let tlHintClosed: boolean; let tlNotesCount = 0; const queue = ref(0); -const prepend = (note) => { +const prepend = (note: entities.Note) => { tlNotesCount++; tlComponent.value?.pagingComponent?.prepend(note); emit("note"); if (props.sound) { - sound.play(isSignedIn && note.userId === me.id ? "noteMy" : "note"); + sound.play(isSignedIn && note.userId === me?.id ? "noteMy" : "note"); } }; @@ -169,14 +184,17 @@ if (props.src === "antenna") { query = { fileId: props.fileId, }; +} else { + throw "NoEndpointError"; } const stream = useStream(); function connectChannel() { if (props.src === "antenna") { + if (!props.antenna) throw "NoAntennaProvided"; connection = stream.useChannel("antenna", { - antennaId: props.antenna!, + antennaId: props.antenna, }); } else if (props.src === "home") { connection = stream.useChannel("homeTimeline", { @@ -265,8 +283,8 @@ function reloadTimeline() { }); } -const pagination: Paging = { - endpoint: endpoint as keyof Endpoints, +const pagination = { + endpoint, limit: 10, params: query, }; diff --git a/packages/client/src/pages/about.federation.vue b/packages/client/src/pages/about.federation.vue index 06a9db2f50..71b4f46de7 100644 --- a/packages/client/src/pages/about.federation.vue +++ b/packages/client/src/pages/about.federation.vue @@ -113,10 +113,11 @@ import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue"; import FormSplit from "@/components/form/split.vue"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { instanceSortParam } from "firefish-js"; const host = ref(""); const state = ref("federating"); -const sort = ref("+pubSub"); +const sort = ref<(typeof instanceSortParam)[number]>("+pubSub"); const pagination = { endpoint: "federation/instances" as const, limit: 10, diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index ce4fc1f58d..c4d9e6ec40 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -94,7 +94,7 @@ const origin = ref("local"); const type = ref(null); const searchHost = ref(""); const userId = ref(""); -const viewMode = ref("grid"); +const viewMode = ref<"list" | "grid">("grid"); const pagination = { endpoint: "admin/drive/files" as const, limit: 10, diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index e56704879c..a065120645 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -313,7 +313,7 @@ const isSilenced = ref(false); const faviconUrl = ref<string | null>(null); const usersPagination = { - endpoint: isAdmin ? "admin/show-users" : ("users" as const), + endpoint: isAdmin ? ("admin/show-users" as const) : ("users" as const), limit: 10, params: { sort: "+updatedAt", diff --git a/packages/client/src/pages/note-history.vue b/packages/client/src/pages/note-history.vue index aaf8519728..fdf8178fbf 100644 --- a/packages/client/src/pages/note-history.vue +++ b/packages/client/src/pages/note-history.vue @@ -8,12 +8,12 @@ <MkPagination v-else ref="pagingComponent" - v-slot="{ items }: { items: entities.NoteEdit[] }" + v-slot="{ items }" :pagination="pagination" > <div ref="tlEl" class="giivymft noGap"> <XList - v-slot="{ item }: { item: entities.Note }" + v-slot="{ item }" :items="convertNoteEditsToNotes(items)" class="notes" :no-gap="true" @@ -35,7 +35,6 @@ <script lang="ts" setup> import { computed, onMounted, ref } from "vue"; import MkPagination from "@/components/MkPagination.vue"; -import type { Paging } from "@/components/MkPagination.vue"; import { api } from "@/os"; import XList from "@/components/MkDateSeparatedList.vue"; import XNote from "@/components/MkNote.vue"; @@ -50,7 +49,7 @@ const props = defineProps<{ noteId: string; }>(); -const pagination: Paging = { +const pagination = { endpoint: "notes/history" as const, limit: 10, offsetMode: true, diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts index d5002c086d..d3564573f0 100644 --- a/packages/firefish-js/src/api.types.ts +++ b/packages/firefish-js/src/api.types.ts @@ -38,6 +38,8 @@ import type { UserSorting, } from "./entities"; +import type * as consts from "./consts"; + type TODO = Record<string, any> | null; type NoParams = Record<string, never>; @@ -84,7 +86,7 @@ export type Endpoints = { "admin/server-info": { req: TODO; res: TODO }; "admin/show-moderation-logs": { req: TODO; res: TODO }; "admin/show-user": { req: TODO; res: TODO }; - "admin/show-users": { req: TODO; res: TODO }; + "admin/show-users": { req: TODO; res: User[] }; "admin/silence-user": { req: TODO; res: TODO }; "admin/suspend-user": { req: TODO; res: TODO }; "admin/unsilence-user": { req: TODO; res: TODO }; @@ -101,7 +103,18 @@ export type Endpoints = { "admin/announcements/update": { req: TODO; res: TODO }; "admin/drive/clean-remote-files": { req: TODO; res: TODO }; "admin/drive/cleanup": { req: TODO; res: TODO }; - "admin/drive/files": { req: TODO; res: TODO }; + "admin/drive/files": { + req: { + limit?: number; + sinceId?: DriveFile["id"]; + untilId?: DriveFile["id"]; + userId?: User["id"]; + type?: string; + origin?: "combined" | "local" | "remote"; + hostname?: string; + }; + res: DriveFile[]; + }; "admin/drive/show-file": { req: TODO; res: TODO }; "admin/emoji/add": { req: TODO; res: TODO }; "admin/emoji/copy": { req: TODO; res: TODO }; @@ -200,7 +213,7 @@ export type Endpoints = { "channels/owned": { req: TODO; res: TODO }; "channels/pin-note": { req: TODO; res: TODO }; "channels/show": { req: TODO; res: TODO }; - "channels/timeline": { req: TODO; res: TODO }; + "channels/timeline": { req: TODO; res: Note[] }; "channels/unfollow": { req: TODO; res: TODO }; "channels/update": { req: TODO; res: TODO }; @@ -238,7 +251,7 @@ export type Endpoints = { }; res: DriveFile[]; }; - "drive/files/attached-notes": { req: TODO; res: TODO }; + "drive/files/attached-notes": { req: TODO; res: Note[] }; "drive/files/check-existence": { req: TODO; res: TODO }; "drive/files/create": { req: TODO; res: TODO }; "drive/files/delete": { req: { fileId: DriveFile["id"] }; res: null }; @@ -360,25 +373,7 @@ export type Endpoints = { publishing?: boolean | null; limit?: number; offset?: number; - sort?: - | "+pubSub" - | "-pubSub" - | "+notes" - | "-notes" - | "+users" - | "-users" - | "+following" - | "-following" - | "+followers" - | "-followers" - | "+caughtAt" - | "-caughtAt" - | "+lastCommunicatedAt" - | "-lastCommunicatedAt" - | "+driveUsage" - | "-driveUsage" - | "+driveFiles" - | "-driveFiles"; + sort?: (typeof consts.instanceSortParam)[number]; }; res: Instance[]; }; diff --git a/packages/firefish-js/src/consts.ts b/packages/firefish-js/src/consts.ts index 16328ceb84..0684d6c08f 100644 --- a/packages/firefish-js/src/consts.ts +++ b/packages/firefish-js/src/consts.ts @@ -151,3 +151,24 @@ export const languages = [ "yi", "zh", ] as const; + +export const instanceSortParam = [ + "+pubSub", + "-pubSub", + "+notes", + "-notes", + "+users", + "-users", + "+following", + "-following", + "+followers", + "-followers", + "+caughtAt", + "-caughtAt", + "+lastCommunicatedAt", + "-lastCommunicatedAt", + "+driveUsage", + "-driveUsage", + "+driveFiles", + "-driveFiles", +] as const; diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index ef1804ee71..c5f09fb2d6 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -1,3 +1,5 @@ +import type * as consts from "./consts"; + export type ID = string; export type DateString = string; @@ -108,7 +110,7 @@ export type MeDetailed = UserDetailed & { isExplorable: boolean; mutedWords: string[][]; mutedPatterns: string[]; - mutingNotificationTypes: string[]; + mutingNotificationTypes: (typeof consts.notificationTypes)[number][]; noCrawle: boolean; preventAiLearning: boolean; receiveAnnouncementEmail: boolean; @@ -129,6 +131,8 @@ export type DriveFile = { blurhash: string; comment: string | null; properties: Record<string, any>; + userId?: User["id"]; + user?: User; }; export type DriveFolder = TODO; @@ -152,7 +156,8 @@ export type Note = { visibleUserIds?: User["id"][]; lang?: string; localOnly?: boolean; - channel?: Channel["id"]; + channelId?: Channel["id"]; + channel?: Channel; myReaction?: string; reactions: Record<string, number>; renoteCount: number; @@ -199,82 +204,98 @@ export type NoteReaction = { type: string; }; -export type Notification = { +interface BaseNotification { id: ID; createdAt: DateString; isRead: boolean; -} & ( - | { - type: "reaction"; - reaction: string; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "reply"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "renote"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "quote"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "mention"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "pollVote"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "pollEnded"; - user: User; - userId: User["id"]; - note: Note; - } - | { - type: "follow"; - user: User; - userId: User["id"]; - } - | { - type: "followRequestAccepted"; - user: User; - userId: User["id"]; - } - | { - type: "receiveFollowRequest"; - user: User; - userId: User["id"]; - } - | { - type: "groupInvited"; - invitation: UserGroup; - user: User; - userId: User["id"]; - } - | { - type: "app"; - header?: string | null; - body: string; - icon?: string | null; - } -); + type: (typeof consts.notificationTypes)[number]; +} + +export interface ReactionNotification extends BaseNotification { + type: "reaction"; + reaction: string; + user: User; + userId: User["id"]; + note: Note; +} +export interface ReplyNotification extends BaseNotification { + type: "reply"; + user: User; + userId: User["id"]; + note: Note; +} +export interface RenoteNotification extends BaseNotification { + type: "renote"; + user: User; + userId: User["id"]; + note: Note; +} +export interface QuoteNotification extends BaseNotification { + type: "quote"; + user: User; + userId: User["id"]; + note: Note; +} +export interface MentionNotification extends BaseNotification { + type: "mention"; + user: User; + userId: User["id"]; + note: Note; +} +export interface PollVoteNotification extends BaseNotification { + type: "pollVote"; + user: User; + userId: User["id"]; + note: Note; +} +export interface PollEndedNotification extends BaseNotification { + type: "pollEnded"; + user: User; + userId: User["id"]; + note: Note; +} +export interface FollowNotification extends BaseNotification { + type: "follow"; + user: User; + userId: User["id"]; +} + +export interface FollowRequestAcceptedNotification extends BaseNotification { + type: "followRequestAccepted"; + user: User; + userId: User["id"]; +} +export interface ReceiveFollowRequestNotification extends BaseNotification { + type: "receiveFollowRequest"; + user: User; + userId: User["id"]; +} +export interface GroupInvitedNotification extends BaseNotification { + type: "groupInvited"; + invitation: UserGroup; + user: User; + userId: User["id"]; +} +export interface AppNotification extends BaseNotification { + type: "app"; + header?: string | null; + body: string; + icon?: string | null; +} + +export type Notification = + | ReactionNotification + | ReplyNotification + | RenoteNotification + | QuoteNotification + | MentionNotification + | PollVoteNotification + | PollEndedNotification + | FollowNotification + | FollowRequestAcceptedNotification + | ReceiveFollowRequestNotification + | GroupInvitedNotification + | AppNotification; export type MessagingMessage = { id: ID; @@ -451,6 +472,7 @@ export type FollowRequest = { export type Channel = { id: ID; + name: string; // TODO }; diff --git a/packages/firefish-js/src/index.ts b/packages/firefish-js/src/index.ts index cb08fad8ab..6639985481 100644 --- a/packages/firefish-js/src/index.ts +++ b/packages/firefish-js/src/index.ts @@ -4,6 +4,7 @@ import { Endpoints } from "./api.types"; import * as consts from "./consts"; import Stream, { Connection } from "./streaming"; import * as StreamTypes from "./streaming.types"; +import type * as TypeUtils from "./type-utils"; export { Endpoints, @@ -12,6 +13,7 @@ export { StreamTypes, acct, type Acct, + type TypeUtils, }; export const permissions = consts.permissions; @@ -20,6 +22,7 @@ export const noteVisibilities = consts.noteVisibilities; export const mutedNoteReasons = consts.mutedNoteReasons; export const languages = consts.languages; export const ffVisibility = consts.ffVisibility; +export const instanceSortParam = consts.instanceSortParam; // api extractor not supported yet //export * as api from './api'; diff --git a/packages/firefish-js/src/streaming.types.ts b/packages/firefish-js/src/streaming.types.ts index c0b0b030cd..e9be8622af 100644 --- a/packages/firefish-js/src/streaming.types.ts +++ b/packages/firefish-js/src/streaming.types.ts @@ -13,6 +13,10 @@ import type { type FIXME = any; +type TimelineParams = { + withReplies?: boolean; +}; + export type Channels = { main: { params: null; @@ -56,35 +60,35 @@ export type Channels = { receives: null; }; homeTimeline: { - params: null; + params?: TimelineParams; events: { note: (payload: Note) => void; }; receives: null; }; localTimeline: { - params: null; + params: TimelineParams; events: { note: (payload: Note) => void; }; receives: null; }; hybridTimeline: { - params: null; + params: TimelineParams; events: { note: (payload: Note) => void; }; receives: null; }; recommendedTimeline: { - params: null; + params: TimelineParams; events: { note: (payload: Note) => void; }; receives: null; }; globalTimeline: { - params: null; + params: TimelineParams; events: { note: (payload: Note) => void; }; diff --git a/packages/firefish-js/src/type-utils.ts b/packages/firefish-js/src/type-utils.ts new file mode 100644 index 0000000000..6c1a67ac59 --- /dev/null +++ b/packages/firefish-js/src/type-utils.ts @@ -0,0 +1,13 @@ +import type { Endpoints } from "./api.types"; + +export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends < + T, +>() => T extends Y ? 1 : 2 + ? true + : false; + +export type PropertyOfType<Type, U> = { + [K in keyof Type]: Type[K] extends U ? K : never; +}[keyof Type]; + +export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;