Typescript: Gör alla egenskaper valfria om inte uttryckligen angivna när du använder en literaltyp


Jag försöker skriva in sökändpunkten för Spotifys API.

I deras dokumentation står det att returdata är

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.
}

Dokumentationen antyder dock att den verkliga typningen av svaret är: "vi kommer endast att returnera de nycklar som du frågade efter".

Det översätts till: om listan är en litteral typ, kan vi begränsa typningen och göra egenskaperna obligatoriska.

Nu är mitt problem: om listan inte är en litteral typ, hur kan vi tvinga svaret att ändå returnera hela typen med alla egenskaper markerade som valfria?

I princip är det jag försöker göra:

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

Är det här ens genomförbart?

Här är mitt försök: lekplats

Svar #1

Först och främst, låt oss se hur vi kan kontrollera om arrayen är en tuple (bokstavlig typ) eller inte.

Skillnaden ligger i längden på arraytypen. I fallet med tuples är det ett ändligt antal. Låt oss först titta på något:

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

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

1 utvidgar number eftersom det är en underkategori av number, men inte tvärtom.

Exempel med arrayer/tupler:

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

Således kan vi dra slutsatsen att om length på arrayen är ett ändligt tal, så är det en tupel, annars är det en array vars exakta längd vi inte kan bestämma. Låt oss skriva en hjälptype för det:

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

Observera att vi använder readonly-modifieraren för att också acceptera readonly-arrayer/tuppler, eftersom readonly-versionen av en array/tuppel är en supertyp av den arrayen:

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

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

I search, måste vi ändra den generiska parametern T så att den begränsas av readonly SearchType[] istället för SearchType. Dessutom behöver vi göra T till en konstant typparameter för att förhindra att kompilatorn utvidgar array-typer till en array av primitiva typer. Observera att const type parameters lades till i Typescript >= 5.0 och om du har en lägre version måste du använda const-assertion när du skickar arrayen till funktionen. Vi behöver dessa ändringar för att kunna använda IsTuple, och anledningen till att lägga till readonly nämndes redan tidigare:

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

Vi kommer att använda indexerad åtkomst för att hämta elementen i T:

T[number]

För att undvika kodupprepning kommer vi att lagra resultatet av valet i den härledda parametern R med hjälp av nyckelordet infer:

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

Nästa steg är att vi ska se om T är en tupel, och om det är så kommer vi göra R icke-valfritt, annars returnera det som det är:

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

Och vi är redo att gå:

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;

Testning :

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

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

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

Länk till lekplats

Relaterade artiklar