首页 > 解决方案 > 使用有区别的联合避免代码重复

问题描述

说我有这个功能

async function getTracksOrAlbums(
  tracksOrAlbums: {tracks: Track[]} | {albums: Album[]},
  notFound: NotFound,
): Promise<GetTracksOrAlbums> {
  if ('albums' in tracksOrAlbums) {
    const albumsResponse = await getManyAlbums(tracksOrAlbums.albums)

    const result = albumsResponse.filter((item, i): item is AlbumResponse => {
      return filterErrors(() => {
        notFound.total++
        notFound.data.push(tracksOrAlbums.albums[i])
      }, item)
    })
    return {
      data: result,
      report: {found: result.length, notFound},
    }
  }

  const tracksResponse = await getManyTracks(tracksOrAlbums.tracks)
  const result = tracksResponse.filter((item, i): item is TrackResponse => {
    return filterErrors(() => {
      notFound.total++
      notFound.data.push(tracksOrAlbums.tracks[i])
    }, item)
  })

  return {
    data: result,
    report: {found: result.length, notFound},
  }
}

我想将以下部分抽象为一个单独的函数:

const albumsResponse = await getManyAlbums(tracksOrAlbums.albums)
const result = albumsResponse.filter((item, i): item is AlbumResponse => {
  return filterErrors(() => {
    notFound.total++
    notFound.data.push(tracksOrAlbums.albums[i])
  }, item)
})

在几乎伪代码中,我想做以下几行:

async function filterTracksOrAlbums<T extends Track | Album>(
  tracksOrAlbums: Array<T>,
  notFound: NotFound,
) {
  const searchRes = await getManyTracks(tracksOrAlbums) // or await getManyAlbums(tracksOrAlbums)
  const result = searchRes.filter(
    // I don't really know how to define here a generic type guard
    (item, i): item is T => {
      //
      return filterErrors(() => {
        notFound.total++
        notFound.data.push(tracksOrAlbums[i])
      }, item)
    },
  )
  return result
}

我还想表达输入之间的关系

(Track | Album)[]

和输出:

(TrackResponse | AlbumResponse)[]

不会导致重复,因为如果我要为播放列表用户 数据添加另一个案例,我将不得不在每次filter回调时重写,并且它是对应的类型保护来匹配一个可能的PlaylistResponseUserResponse

有没有办法封装这段代码而不会导致这种重复?我开始使用 函数重载,但使代码变得不必要地复杂。


getManyAlbums并且getManyTracks是对 API 执行请求的函数,可以模拟为:

function getManyAlbums(
  albums: Album[],
): Promise<(AlbumResponse | ErrorResponse)[]> {
  return Promise.resolve([
    {ok: false, message: 'error message'},
    {ok: true, data: {id: '1', name: 'bar', artist: 'foo'}},
  ])
}

function getManyTracks(
  tracks: Track[],
): Promise<(TrackResponse | ErrorResponse)[]> {
  return Promise.resolve([
    {ok: true, data: {id: '1', name: 'bar', artist: 'foo', isrc: 'baz'}},
    {ok: false, message: 'error message'},
  ])
}

这些是相关的类型声明

type Track = {artist: string; song: string}
type Album = {artist: string; album: string}

type TrackResponse = {
  ok: true
  data: {id: string; name: string; artist: string; isrc: string}
}
type AlbumResponse = {
  ok: true
  data: {id: string; name: string; artist: string}
}

type ErrorResponse = {ok: false; message: string}

type NotFound = {total: number; data: unknown[]}
type GetReturnType<T> = {
  data: T
  report: {found: number; notFound: NotFound}
}

type GetTracks = GetReturnType<TrackResponse[]>
type GetAlbums = GetReturnType<AlbumResponse[]>
type GetTracksOrAlbums = GetTracks | GetAlbums

标签: typescriptoverloadingdry

解决方案


如果您操作的数据结构尽可能相似,我认为您会发现将通用功能抽象为单个函数会更容易。与其使用不同的来区分某事物是与 aAlbum还是 a相关Track,不如使用不同的来执行此操作。

例如,输入输出映射如下所示:

// Input types
interface TrackInput { artist: string; song: string }
interface AlbumInput { artist: string; album: string }

// Output Types
interface TrackOutput { id: string; name: string; artist: string; isrc: string }
interface AlbumOutput { id: string; name: string; artist: string }

interface DataIO {
  albums: { input: AlbumInput, output: AlbumOutput },
  tracks: { input: TrackInput, output: TrackOutput }
}

DataIO类型只是为了让编译器跟踪哪个输入与哪个输出。如果我们可以仅用这些类型来表示您的功能,那么我们就有机会通用地DataIO这样做。


我们可以像这样定义输出:

interface Response<T> { ok: true, data: T }
interface ErrorResponse { ok: false; message: string }
type ResponseOrError<T> = (Response<T> | ErrorResponse)[]

对于输入,我们可以将原始的{albums: AlbumInput[]}or拆分{tracks: TrackInput[]}为两个参数:一个type参数"albums"or "tracks",以及一个or的searchData参数,具体取决于.AlbumInput[]TrackInput[]type

这看起来像:

declare function getManyThings<K extends keyof DataIO>(
  type: K,
  searchData: Array<DataIO[K]['input']>
): Promise<ResponseOrError<DataIO[K]['output']>>;

这是一个泛型函数,其中K的类型是type,我们使用索引访问类型来为searchData.

根据现有函数实现它需要一些类型断言等,因为编译器无法真正遵循返回值实际上与声明的返回类型匹配。它可以看到这type是一个特定的值,但这对K. 有关请求支持此类功能的问题,请参阅microsoft/TypeScript#33014 。无论如何,它可能是这样的:

function getManyThings<K extends keyof DataIO>(
  type: K,
  searchData: Array<DataIO[K]['input']>
): Promise<ResponseOrError<DataIO[K]['output']>> {
  return type === "albums" ? 
    getManyAlbums(searchData as AlbumInput[]) : 
    getManyTracks(searchData as TrackInput[]);
}

然后你可以filterTracksOrAlbums这样写,没有任何额外的问题:

async function filterTracksOrAlbums<K extends keyof DataIO>(
  type: K,
  tracksOrAlbums: Array<DataIO[K]['input']>,
  notFound: NotFound,
) {
  const searchRes = await getManyThings(type, tracksOrAlbums);

  const result = searchRes.filter(
    (item, i): item is Response<DataIO[K]['output']> => {
      return filterErrors(() => {
        notFound.total++
        notFound.data.push(tracksOrAlbums[i])
      }, item)
    },
  )
  return result
}

请注意,由于在要搜索的数据的filterTracksOrAlbums()中也是泛型的K,我们可以将过滤器代码的类型谓词泛型表示为。typeitem is Response<DataIO[K]['output']>

Playground 代码链接


推荐阅读