typescript - 使用有区别的联合避免代码重复
问题描述
说我有这个功能
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
回调时重写,并且它是对应的类型保护来匹配一个可能的PlaylistResponse
和UserResponse
有没有办法封装这段代码而不会导致这种重复?我开始使用 函数重载,但使代码变得不必要地复杂。
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
解决方案
如果您操作的数据结构尽可能相似,我认为您会发现将通用功能抽象为单个函数会更容易。与其使用不同的键来区分某事物是与 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
,我们可以将过滤器代码的类型谓词泛型表示为。type
item is Response<DataIO[K]['output']>
推荐阅读
- xml - 即使 contains() 不匹配,xmllint 也会选择一个节点
- python - numpy中的Python变量赋值
- android - 考虑到 Android O 在 Android 中创建通知的方法
- database - 在 NHibernate 事件监听器中处理复合主键
- javascript - 如何在另一个包中重用 webpack 包?
- python - 用于变量的循环列表 Python
- android - 如何在特定时间向特定用户发送通知
- forms - 在 AEM 自适应表单中为必填字段添加星号
- javascript - 使用 JS 从网络服务器下载 PDF 文件,并将它们保存在 iOS 7 上
- angular - Angular HttpClient 无法获得响应