Typescript:使所有属性都是可选的,除非在使用文字类型时显式指定

c9x0cxw0  于 2023-06-30  发布在  TypeScript
关注(0)|答案(1)|浏览(124)

我正在尝试输入search endpoint of Spotify's API
在他们的文档中,它说返回的有效载荷是

export enum ResourceType {
  Album = 'album',
  Artist = 'artist',
  Playlist = 'playlist',
  Track = 'track',
  Show = 'show',
  Episode = 'episode',
  Audiobook = 'audiobook',

  Albums = 'albums',
  Artists = 'artists',
  Playlists = 'playlists',
  Tracks = 'tracks',
  Shows = 'shows',
  Episodes = 'episodes',
  Audiobooks = 'audiobooks',
}

interface ResourceTypeToResultKey {
  [ResourceType.Album]: ResourceType.Albums
  [ResourceType.Artist]: ResourceType.Artists
  [ResourceType.Track]: ResourceType.Tracks
  [ResourceType.Playlist]: ResourceType.Playlists
  [ResourceType.Show]: ResourceType.Shows
  [ResourceType.Episode]: ResourceType.Episodes
  [ResourceType.Audiobook]: ResourceType.Audiobooks
}

// Basically translates to -> Make every property possibly undefined
type SearchResults = {
  [K in SearchType as ResourceTypeToResultKey[K]]?: unknown // Don't pay attention to the value, I got it fine.
}

但是,文档建议响应的真实的类型是:* “我们只会归还您要求的钥匙”*。
这就意味着:如果列表是文字类型,我们可以缩小类型并使属性成为必需的。
现在我的问题是:如果列表不是一个文字类型,我们如何强制响应仍然返回整个类型,所有属性都标记为可选?
基本上,我想做的是:

declare const search: <T extends SearchType>(types: T[]) => Required<Pick<SearchResults, ResourceTypeToResultKey[T]>>
declare const types: SearchType[]
const result = search(types)
// the given type should be `SearchResults` (all properties marked optional)
result.tracks // Not ok, should be optional as it can't be determined for sure

const result2 = search([ResourceType.Track])
// the given type should be `{ tracks: unknown }`
result2.tracks // ok

这可行吗?
以下是我的尝试:Playground

jxct1oxe

jxct1oxe1#

首先,让我们看看如何检查数组是否是元组(文字类型)。
不同之处在于数组类型的长度。在元组的情况下,它是一个有限的数字。我们先来看看:

type Case1 = 1 extends number ? true : false
//   ^? true

type Case2 = number extends 1 ? true : false
//   ^? false

1扩展了number,因为它是number的子类型,但不是相反。
使用数组/元组的示例:

type Case1 = [1, 2, 3]["length"];
//   ^? 3

type Case2 = (readonly [1, 2, 3])["length"];
//   ^? 3

type Case3 = number[]["length"];
//   ^? number

type Case4 = (readonly number[])["length"];
//   ^? number

因此,我们可以得出结论,如果数组的length是一个有限数,那么它是一个元组,否则它是一个数组,我们无法确定它的确切长度。让我们为此编写一个实用程序类型:

type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true

注意,我们使用readonly修饰符来接受readonly数组/元组,因为readonly版本的数组/元组是该数组的超类型:

type Case1 = readonly number[] extends number[] ? true : false;
//   ^? false

type Case2 = number[] extends readonly number[] ? true : false;
//   ^? true

search,中,我们必须修改泛型参数T,使其受readonly SearchType[]而不是SearchType的约束。此外,我们需要将T转换为const类型参数,以防止编译器将数组类型扩展为原始数组。请注意,const type parameters是在Typescript >= 5.0中添加的,如果您有较低版本,则需要在将数组传入函数时使用constAssert。我们需要这些更改才能使用IsTuple,添加readonly的原因在前面已经提到过:

declare const search: <const T extends readonly SearchType[]>(types: T) => ToBeDefined

我们将在indexed access中得到T的元素:

T[number]

为了避免代码重复,我们将使用inert关键字将pick的结果存储在推断参数R中:

Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R 
 ? DoSomething
 : never // in theory, never will be reached

接下来,我们将看看T是否是一个元组,如果是,我们将使R为非可选的,否则返回原样:

Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R
  ? IsTuple<T> extends false
    ? R
    : Required<R>
  : never;

我们准备好了:

declare const search: <const T extends readonly SearchType[]>(
  types: T
) => Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R
  ? IsTuple<T> extends false
    ? R
    : Required<R>
  : never;

测试:

declare const types: SearchType[];
const result = search(types);
result.tracks; // (property) tracks?: unknown

const result2 = search([ResourceType.Track]);

result2.tracks; // (property) tracks: unknown

链接到Playground

相关问题