英文:
Typescript: Make all properties optional unless explicited when using a literal type
问题
我正在尝试输入Spotify 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)
// 给定类型应为`SearchResults`(所有属性标记为可选)
result.tracks // 不可以,应该是可选的,因为无法确定
const result2 = search([ResourceType.Track])
// 给定类型应为`{ tracks: unknown }`
result2.tracks // 可以
这可行吗?
这是我的尝试:playground
英文:
I am trying to type the search endpoint of Spotify's API.
In their documentation, it says that the return payload is
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.
}
However, the documentation suggests that the real typing of the response is: "we will only return the keys that you asked for".
That translates to: if the list is a literal type, we can narrow the typing and make the properties required.
Now my issue is: if the list is not a literal type, how can we force the response to still return the whole type with all properties marked optional ?
Basically, what I'm trying to do is:
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
Is this even feasible ?
Here is what my attempt: playground
答案1
得分: 4
首先,让我们看看如何检查数组是否为元组(字面类型)。
区别在于数组类型的长度。对于元组,它是一个有限的数字。让我们先看一些东西:
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
数组/元组,因为只读版本的数组/元组是该数组的超级类型:
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 类型参数
是在 TypeScript >= 5.0
中添加的,如果您使用较低版本,将需要在将数组传递到函数时使用 const 断言。我们需要这些更改才能使用 IsTuple
,而之前已经提到过添加 readonly
的原因:
declare const search: <const T extends readonly SearchType[]>(types: T) => ToBeDefined
我们将使用 索引访问 来获取 T
的元素:
T[number]
为了避免代码重复,我们将在推断的参数 R
中存储选取的结果,使用 infer 关键字:
Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R
? DoSomething
: never // 理论上,永远不会到达
接下来,我们将查看 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
英文:
First of all, let's see how we can check whether the array is a tuple (literal type) or not.
The difference is in the length of the array type. In the case of tuples, it is a finite number. Let's see something first:
type Case1 = 1 extends number ? true : false
// ^? true
type Case2 = number extends 1 ? true : false
// ^? false
1
extends number
since it is a sub-type of number
, but not the reverse.
Examples with arrays / tuples:
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
Thus, we can conclude that if the length
of the array is a finite number then it is a tuple, otherwise it is an array, the exact length of which we can't determine. Let's write a utility type for that:
type IsTuple<T extends readonly unknown[]> = number extends T['length'] ? false : true
Note that we are using readonly
modifier to accept readonly
arrays/tuples as well, since readonly
version of an array / tuple is a super type of that array:
type Case1 = readonly number[] extends number[] ? true : false;
// ^? false
type Case2 = number[] extends readonly number[] ? true : false;
// ^? true
In the search,
we must modify the generic parameter T
to be constrained by readonly SearchType[]
instead of SearchType
. Also, we need to turn T
into const type parameter to prevent the compiler from widening array types to an array of primitives. Note that const type parameters
were added in Typescript >= 5.0
and if you have the lower version you will need to use const assertion when you pass the array into function. We need these changes to be able to use IsTuple
, and the reason to add readonly
was already mentioned previously:
declare const search: <const T extends readonly SearchType[]>(types: T) => ToBeDefined
We are going to indexed access to get the elements of T
:
T[number]
To avoid code repetition we are going to store the result of the pick in the inferred parameter R
using the infer keyword:
Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R
? DoSomething
: never // in theory, never will be reached
Next, we are going to see if T
is a tuple, and if yes we will make the R
non-optional, otherwise return it as it is:
Pick<SearchResults, ResourceTypeToResultKey[T[number]]> extends infer R
? IsTuple<T> extends false
? R
: Required<R>
: never;
And we are good to go:
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;
Testing:
declare const types: SearchType[];
const result = search(types);
result.tracks; // (property) tracks?: unknown
const result2 = search([ResourceType.Track]);
result2.tracks; // (property) tracks: unknown
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论