避免有区别的联合的代码重复
Avoiding code repetition with discriminated unions
说我有这个功能
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)[]
不会引起重复,因为如果我要为 playlists 或 user data[= 添加另一个案例51=],
每次 filter
回调和它对应的 类型时,我都必须重写
guard 匹配可能的 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
我认为如果您操作的数据结构尽可能相似,您会发现将通用功能抽象为单个函数会更容易。与其使用不同的 keys 来区分某事是与 Album
还是 Track
相关,不如使用不同的 values 来做到这一点。
例如,输入输出映射看起来像这样:
// 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"
,以及AlbumInput[]
或 TrackInput[]
的 searchData
参数,具体取决于 type
.
看起来像:
declare function getManyThings<K extends keyof DataIO>(
type: K,
searchData: Array<DataIO[K]['input']>
): Promise<ResponseOrError<DataIO[K]['output']>>;
这是一个通用函数,其中 K
是 type
的类型,我们使用 indexed access types 将正确的类型赋予 searchData
。
根据现有功能实施该功能将需要一些 type assertions or the like, since the compiler can't really follow that the return value will actually match the declared return type. It can see that type
is a particular value, but this doesn't have any effect on K
. See 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<DataIO[K]['output']>
.
说我有这个功能
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)[]
不会引起重复,因为如果我要为 playlists 或 user data[= 添加另一个案例51=],
每次 filter
回调和它对应的 类型时,我都必须重写
guard 匹配可能的 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
我认为如果您操作的数据结构尽可能相似,您会发现将通用功能抽象为单个函数会更容易。与其使用不同的 keys 来区分某事是与 Album
还是 Track
相关,不如使用不同的 values 来做到这一点。
例如,输入输出映射看起来像这样:
// 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"
,以及AlbumInput[]
或 TrackInput[]
的 searchData
参数,具体取决于 type
.
看起来像:
declare function getManyThings<K extends keyof DataIO>(
type: K,
searchData: Array<DataIO[K]['input']>
): Promise<ResponseOrError<DataIO[K]['output']>>;
这是一个通用函数,其中 K
是 type
的类型,我们使用 indexed access types 将正确的类型赋予 searchData
。
根据现有功能实施该功能将需要一些 type assertions or the like, since the compiler can't really follow that the return value will actually match the declared return type. It can see that type
is a particular value, but this doesn't have any effect on K
. See 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<DataIO[K]['output']>
.