Typescript:在使用字面类型时,除非明确指定,否则使所有属性可选。

huangapple go评论63阅读模式
英文:

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

[Playground 链接](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBMAdgVwLZwErAM4WVAY2ABUBPMYOAbwCg44BBAGwCM04BeOAIgENW03ADR1GsAJbZ4XPhKnDRABSa9STSdJ5gVajQvrEovAgGtOPGEdP64AZQAWEAO7nu2R05sBRMJIgATShlgX1xAmwZkf3EIFggIMxleKJi4hIVRZjZUbFd+bOwIuRhcpOLCkXplVXUpUq0dWpKbQ2MTeu5LNorRB2cO937vUICcVxC-QJ76SOjY+Pa8lPmEntEAVWxgKFdkLagFAF8aUEhYOBhySltgXkJ7MgpOUQAfTBw8QhIrgDostFe71w+CIj2Af2KgKwwK+YJ+1V0UihHxB3woP1apmRMNBvz6Tmxn1x6J8k2AhNRcNmqQWAG4aDRxIgYNsAGbGSjQolokgQaHIJgwADSwFI1FEAG0uZTfv9UABdABcQO5VIEOXp9ClKNhsuKSpVMvRDHKmrg2pxPIxVhMBuluvRmPaZotqt+CKadp1xPBHo02Bd9p9P3xXstcPxAclQatpLCwDDbpJIymge9VupK1typjVOWaWdNGONAA9CW4AAhXjYcQEfhMMVdRDYFQs3IwCBwAC0AD44ABZXgmSjAABu2zFYCgEAosEnEGwNZYDbgyEQgVZTOA-holyeNzuBHs-MF9VoWqFcCZdlu9zBcGrhodvJPwtFEqF8vlAH5lWuTIgziILScBlnAAAiECIAA5PAY

英文:

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][&quot;length&quot;];
//   ^? 3

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

type Case3 = number[][&quot;length&quot;];
//   ^? number

type Case4 = (readonly number[])[&quot;length&quot;];
//   ^? 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&lt;T extends readonly unknown[]&gt; = number extends T[&#39;length&#39;] ? 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 &gt;= 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: &lt;const T extends readonly SearchType[]&gt;(types: T) =&gt; 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&lt;SearchResults, ResourceTypeToResultKey[T[number]]&gt; 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&lt;SearchResults, ResourceTypeToResultKey[T[number]]&gt; extends infer R
  ? IsTuple&lt;T&gt; extends false
    ? R
    : Required&lt;R&gt;
  : never;

And we are good to go:

declare const search: &lt;const T extends readonly SearchType[]&gt;(
  types: T
) =&gt; Pick&lt;SearchResults, ResourceTypeToResultKey[T[number]]&gt; extends infer R
  ? IsTuple&lt;T&gt; extends false
    ? R
    : Required&lt;R&gt;
  : 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

Link to Playground

huangapple
  • 本文由 发表于 2023年6月26日 02:58:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76551984.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定