【问题标题】:Avoiding code repetition with discriminated unions使用有区别的联合避免代码重复
【发布时间】:2021-07-06 19:11:49
【问题描述】:

说我有这个功能

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)[]

不会导致重复,因为如果我要为 playlistsuser data 添加另一个案例, 每次filter 回调时我都必须重写它是对应的 type 警卫来匹配一个可能的PlaylistResponseUserResponse

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


getManyAlbumsgetManyTracks 是对 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

【问题讨论】:

  • 请考虑修改这个问题中的代码以构成一个minimal reproducible example,当它放入像The TypeScript Playground (link)这样的独立IDE时,清楚地展示了您面临的问题(所以没有未声明或未定义类型或值)。这将使那些想要帮助您的人立即着手解决问题,而无需首先重新创建它。它将使您得到的任何答案都可以针对定义明确的用例进行测试。
  • 我试图简化它并添加运行它所需的代码,但我认为它很长。我希望这次会更清楚。
  • 是的,那里发生了很多事情;您能否尝试将其削减,以便在不包括所有功能的情况下仍然显示重复问题?除非您能提出真正的 generic 抽象,否则您最终将需要进行大量额外的类型操作(重载或断言或辅助函数)才能使其工作,这可能是真的。
  • 因此,您可以执行类似this 的操作,其中您的联合实际上是具有判别属性的可区分联合(本例中为type),然后编写对输入/输出进行操作的通用函数类型地图。但我不知道这对你是否值得。如果是这样,我也许可以写一个答案,但我肯定更愿意在一个更简单的例子中这样做。祝你好运!
  • 好的,我尝试了你的建议,我很喜欢!我最终得到了我在此处标记的代码:Playground 在意识到没有必要之后,我也摆脱了函数重载。但由于我对 TypeScript 比较陌生,所以我不完全确定

标签: typescript overloading dry


【解决方案1】:

如果您操作的数据结构尽可能相似,我认为您会发现将常用功能抽象为单个函数会更容易。与其使用不同的来区分某事物是与Album还是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 来做到这一点generically


我们可以这样定义输出:

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

对于输入,我们可以将原始{albums: AlbumInput[]}{tracks: TrackInput[]} 拆分为两个参数:type 参数"albums""tracks",以及searchData 参数AlbumInput[] 或@ 987654340@,取决于type

看起来像:

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

这是一个通用函数,其中Ktype 的类型,我们使用indexed access typessearchData 提供正确的类型。

在现有函数方面实现这一点将需要一些type assertions 或类似的东西,因为编译器无法真正遵循返回值实际上与声明的返回类型匹配。可以看出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中也是通用的,要搜索的数据的type,我们可以将过滤器代码的type predicate一般表示为item is Response&lt;DataIO[K]['output']&gt;

Playground link to code

【讨论】:

    猜你喜欢
    • 2018-02-01
    • 2011-03-05
    • 2014-03-07
    • 1970-01-01
    • 1970-01-01
    • 2014-07-18
    • 1970-01-01
    相关资源
    最近更新 更多