Framework
Version

TypeScript

TanStack Query ist jetzt in TypeScript geschrieben, um sicherzustellen, dass die Bibliothek und Ihre Projekte typsicher sind!

Zu beachtende Punkte

  • Typen erfordern derzeit die Verwendung von TypeScript v4.7 oder höher
  • Änderungen an Typen in diesem Repository gelten als nicht-brechend und werden normalerweise als Patch-Semver-Änderungen veröffentlicht (andernfalls wäre jede Typverbesserung eine Hauptversion!).
  • Es wird dringend empfohlen, Ihre angular-query-experimental Paketversion auf eine bestimmte Patch-Version zu sperren und ein Upgrade zu erwarten, dass Typen zwischen beliebigen Releases behoben oder aktualisiert werden können
  • Die nicht typbezogene öffentliche API von TanStack Query und - nach der experimentellen Phase - das angular-query-Paket folgen immer noch sehr streng Semver.

Typinferenzen

Typen in TanStack Query fließen im Allgemeinen sehr gut, so dass Sie keine Typannotationen selbst angeben müssen

angular-ts
@Component({
  // ...
  template: `@let data = query.data();`,
  //               ^? data: number | undefined
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
  }))
}
@Component({
  // ...
  template: `@let data = query.data();`,
  //               ^? data: number | undefined
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
  }))
}
angular-ts
@Component({
  // ...
  template: `@let data = query.data();`,
  //               ^? data: string | undefined
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
    select: (data) => data.toString(),
  }))
}
@Component({
  // ...
  template: `@let data = query.data();`,
  //               ^? data: string | undefined
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
    select: (data) => data.toString(),
  }))
}

Dies funktioniert am besten, wenn Ihre queryFn einen gut definierten Rückgabetyp hat. Beachten Sie, dass die meisten Datenabrufbibliotheken standardmäßig any zurückgeben. Stellen Sie daher sicher, dass Sie diese in eine ordnungsgemäß typisierte Funktion extrahieren.

In diesem Beispiel übergeben wir Group[] an den Typparameter der get-Methode von HttpClient.

angular-ts
@Component({
  template: `@let data = query.data();`,
  //               ^? data: Group[] | undefined
})
class MyComponent {
  http = inject(HttpClient)

  query = injectQuery(() => ({
    queryKey: ['groups'],
    queryFn: () => lastValueFrom(this.http.get<Group[]>('/groups')),
  }))
}
@Component({
  template: `@let data = query.data();`,
  //               ^? data: Group[] | undefined
})
class MyComponent {
  http = inject(HttpClient)

  query = injectQuery(() => ({
    queryKey: ['groups'],
    queryFn: () => lastValueFrom(this.http.get<Group[]>('/groups')),
  }))
}

Typ-Verengung

TanStack Query verwendet einen diskriminierten Union-Typ für das Query-Ergebnis, diskriminiert durch das status-Feld und die abgeleiteten booleschen Statusflags. Dies ermöglicht es Ihnen, z. B. den isSuccess()-Status zu prüfen, um data als definiert zu behandeln

angular-ts
@Component({
  // ...
  template: `
    @if (query.isSuccess()) {
      @let data = query.data();
      //    ^? data: number
    }
  `,
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
  }))
}
@Component({
  // ...
  template: `
    @if (query.isSuccess()) {
      @let data = query.data();
      //    ^? data: number
    }
  `,
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(5),
  }))
}

TypeScript unterstützt derzeit keine diskriminierten Unions bei Objektmethoden. Die Verengung auf Signalfelder in Objekten wie Query-Ergebnissen funktioniert nur bei Signalen, die einen booleschen Wert zurückgeben. Bevorzugen Sie die Verwendung von isSuccess() und ähnlichen booleschen Statusignalen gegenüber status() === 'success'.

Typisierung des Fehlerfelds

Der Typ für Fehler ist standardmäßig Error, da dies das ist, was die meisten Benutzer erwarten.

angular-ts
@Component({
  // ...
  template: `@let error = query.error();`,
  //                ^? error: Error | null
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['groups'],
    queryFn: fetchGroups
  }))
}
@Component({
  // ...
  template: `@let error = query.error();`,
  //                ^? error: Error | null
})
class MyComponent {
  query = injectQuery(() => ({
    queryKey: ['groups'],
    queryFn: fetchGroups
  }))
}

Wenn Sie einen benutzerdefinierten Fehler oder etwas werfen möchten, das kein Error ist, können Sie den Typ des Fehlerfelds angeben

angular-ts
@Component({
  // ...
  template: `@let error = query.error();`,
  //                ^? error: string | null
})
class MyComponent {
  query = injectQuery<Group[], string>(() => ({
    queryKey: ['groups'],
    queryFn: fetchGroups,
  }))
}
@Component({
  // ...
  template: `@let error = query.error();`,
  //                ^? error: string | null
})
class MyComponent {
  query = injectQuery<Group[], string>(() => ({
    queryKey: ['groups'],
    queryFn: fetchGroups,
  }))
}

Dies hat jedoch den Nachteil, dass die Typinferenz für alle anderen Generika von injectQuery nicht mehr funktioniert. Es gilt im Allgemeinen nicht als gute Praxis, etwas zu werfen, das kein Error ist. Wenn Sie also eine Unterklasse wie AxiosError haben, können Sie die *Typ-Verengung* verwenden, um das Fehlerfeld spezifischer zu machen

ts
import axios from 'axios'

query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups }))

computed(() => {
  const error = query.error()
  //     ^? error: Error | null

  if (axios.isAxiosError(error)) {
    error
    // ^? const error: AxiosError
  }
})
import axios from 'axios'

query = injectQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups }))

computed(() => {
  const error = query.error()
  //     ^? error: Error | null

  if (axios.isAxiosError(error)) {
    error
    // ^? const error: AxiosError
  }
})

Registrierung eines globalen Fehlers

TanStack Query v5 ermöglicht es, einen globalen Fehlertyp für alles festzulegen, ohne Generika auf der Aufruferseite angeben zu müssen, indem das Register-Interface ergänzt wird. Dies stellt sicher, dass die Inferenz weiterhin funktioniert, aber das Fehlerfeld den angegebenen Typ hat. Wenn Sie erzwingen möchten, dass Aufrufer explizite Typ-Verengung durchführen müssen, setzen Sie defaultError auf unknown

ts
import '@tanstack/angular-query-experimental'

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    // Use unknown so call sites must narrow explicitly.
    defaultError: unknown
  }
}

const query = injectQuery(() => ({
  queryKey: ['groups'],
  queryFn: fetchGroups,
}))

computed(() => {
  const error = query.error()
  //      ^? error: unknown | null
})
import '@tanstack/angular-query-experimental'

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    // Use unknown so call sites must narrow explicitly.
    defaultError: unknown
  }
}

const query = injectQuery(() => ({
  queryKey: ['groups'],
  queryFn: fetchGroups,
}))

computed(() => {
  const error = query.error()
  //      ^? error: unknown | null
})

Typisierung von Meta-Daten

Registrierung globaler Meta-Daten

Ähnlich wie bei der Registrierung eines globalen Fehlertyps können Sie auch einen globalen Meta-Typ registrieren. Dies stellt sicher, dass das optionale meta-Feld auf Queries und Mutationen konsistent bleibt und typsicher ist. Beachten Sie, dass der registrierte Typ von Record<string, unknown> erben muss, damit meta ein Objekt bleibt.

ts
import '@tanstack/angular-query-experimental'

interface MyMeta extends Record<string, unknown> {
  // Your meta type definition.
}

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    queryMeta: MyMeta
    mutationMeta: MyMeta
  }
}
import '@tanstack/angular-query-experimental'

interface MyMeta extends Record<string, unknown> {
  // Your meta type definition.
}

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    queryMeta: MyMeta
    mutationMeta: MyMeta
  }
}

Typisierung von Query- und Mutationsschlüsseln

Registrierung der Typen für Query- und Mutationsschlüssel

Ebenso wie bei der Registrierung eines globalen Fehlertyps können Sie auch einen globalen QueryKey- und MutationKey-Typ registrieren. Dies ermöglicht es Ihnen, Ihren Schlüsseln mehr Struktur zu geben, die der Hierarchie Ihrer Anwendung entspricht, und sie über die gesamte Oberfläche der Bibliothek typsicher zu machen. Beachten Sie, dass der registrierte Typ von Array erben muss, damit Ihre Schlüssel ein Array bleiben.

ts
import '@tanstack/angular-query-experimental'

type QueryKey = ['dashboard' | 'marketing', ...ReadonlyArray<unknown>]

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    queryKey: QueryKey
    mutationKey: QueryKey
  }
}
import '@tanstack/angular-query-experimental'

type QueryKey = ['dashboard' | 'marketing', ...ReadonlyArray<unknown>]

declare module '@tanstack/angular-query-experimental' {
  interface Register {
    queryKey: QueryKey
    mutationKey: QueryKey
  }
}

Typisierung von Query-Optionen

Wenn Sie Query-Optionen in injectQuery inline verwenden, erhalten Sie automatische Typinferenz. Möglicherweise möchten Sie die Query-Optionen jedoch in eine separate Funktion extrahieren, um sie zwischen injectQuery und z. B. prefetchQuery zu teilen oder sie in einem Service zu verwalten. In diesem Fall würden Sie die Typinferenz verlieren. Um sie wiederzugewinnen, können Sie die Hilfsfunktion queryOptions verwenden

ts
@Injectable({
  providedIn: 'root',
})
export class QueriesService {
  private http = inject(HttpClient)

  post(postId: number) {
    return queryOptions({
      queryKey: ['post', postId],
      queryFn: () => {
        return lastValueFrom(
          this.http.get<Post>(
            `https://jsonplaceholder.typicode.com/posts/${postId}`,
          ),
        )
      },
    })
  }
}

@Component({
  // ...
})
export class Component {
  queryClient = inject(QueryClient)

  postId = signal(1)

  queries = inject(QueriesService)
  optionsSignal = computed(() => this.queries.post(this.postId()))

  postQuery = injectQuery(() => this.queries.post(1))
  postQuery = injectQuery(() => this.queries.post(this.postId()))

  // You can also pass a signal which returns query options
  postQuery = injectQuery(this.optionsSignal)

  someMethod() {
    this.queryClient.prefetchQuery(this.queries.post(23))
  }
}
@Injectable({
  providedIn: 'root',
})
export class QueriesService {
  private http = inject(HttpClient)

  post(postId: number) {
    return queryOptions({
      queryKey: ['post', postId],
      queryFn: () => {
        return lastValueFrom(
          this.http.get<Post>(
            `https://jsonplaceholder.typicode.com/posts/${postId}`,
          ),
        )
      },
    })
  }
}

@Component({
  // ...
})
export class Component {
  queryClient = inject(QueryClient)

  postId = signal(1)

  queries = inject(QueriesService)
  optionsSignal = computed(() => this.queries.post(this.postId()))

  postQuery = injectQuery(() => this.queries.post(1))
  postQuery = injectQuery(() => this.queries.post(this.postId()))

  // You can also pass a signal which returns query options
  postQuery = injectQuery(this.optionsSignal)

  someMethod() {
    this.queryClient.prefetchQuery(this.queries.post(23))
  }
}

Darüber hinaus kennt der von queryOptions zurückgegebene queryKey die zugehörige queryFn, und wir können diese Typinformationen nutzen, um Funktionen wie queryClient.getQueryData ebenfalls typsicher zu machen

ts
data = this.queryClient.getQueryData(groupOptions().queryKey)
// ^? data: Post | undefined
data = this.queryClient.getQueryData(groupOptions().queryKey)
// ^? data: Post | undefined

Ohne queryOptions wäre der Datentyp unbekannt, es sei denn, wir würden einen Typparameter übergeben

ts
data = queryClient.getQueryData<Post>(['post', 1])
data = queryClient.getQueryData<Post>(['post', 1])

Typisierung von Mutationsoptionen

Ähnlich wie bei queryOptions können Sie mutationOptions verwenden, um Mutationsoptionen in eine separate Funktion zu extrahieren

ts
export class QueriesService {
  private http = inject(HttpClient)

  updatePost(id: number) {
    return mutationOptions({
      mutationFn: (post: Post) => Promise.resolve(post),
      mutationKey: ['updatePost', id],
      onSuccess: (newPost) => {
        //           ^? newPost: Post
        this.queryClient.setQueryData(['posts', id], newPost)
      },
    })
  }
}
export class QueriesService {
  private http = inject(HttpClient)

  updatePost(id: number) {
    return mutationOptions({
      mutationFn: (post: Post) => Promise.resolve(post),
      mutationKey: ['updatePost', id],
      onSuccess: (newPost) => {
        //           ^? newPost: Post
        this.queryClient.setQueryData(['posts', id], newPost)
      },
    })
  }
}

Typsicheres Deaktivieren von Abfragen mit skipToken

Wenn Sie TypeScript verwenden, können Sie skipToken verwenden, um eine Query zu deaktivieren. Dies ist nützlich, wenn Sie eine Query basierend auf einer Bedingung deaktivieren möchten, aber die Query trotzdem typsicher halten möchten. Lesen Sie mehr dazu im Leitfaden Disabling Queries.