框架
版本

TypeScript

TanStack Query 现在使用 TypeScript 编写,以确保库和您的项目都是类型安全的!

需要注意的事项

  • 当前类型要求使用 TypeScript v4.7 或更高版本
  • 此仓库中类型的更改被视为非破坏性更改,通常作为补丁语义版本更改发布(否则每次类型增强都会是主要版本!)
  • 强烈建议将您的 angular-query-experimental 包版本锁定到特定的补丁版本,并在升级时预期类型会在任何发布之间得到修复或升级
  • TanStack Query 的非类型相关公共 API,以及在实验阶段之后,angular-query 包仍然非常严格地遵循 semver。

类型推断

TanStack Query 中的类型通常会非常流畅地传递,因此您无需自行提供类型注解。

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(),
  }))
}

当您的 queryFn 具有明确定义的返回类型时,效果最佳。请记住,大多数数据获取库默认返回 any,因此请确保将其提取到一个正确类型的函数中。

在此示例中,我们将 Group[] 传递给 HttpClient 的 get 方法的类型参数。

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')),
  }))
}

类型缩小

TanStack Query 使用 判别联合类型 来表示查询结果,该类型由 status 字段和派生的状态布尔标志进行判别。这将允许您检查例如 isSuccess() 状态,使 data 定义。

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 目前不支持对象方法上的判别联合。在对象(如查询结果)上对信号字段进行细化仅适用于返回布尔值的信号。请优先使用 isSuccess() 和类似的布尔状态信号,而不是 status() === 'success'

错误字段的类型定义

error 的类型默认为 Error,因为这是大多数用户所期望的。

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

如果您想抛出自定义错误,或者根本不是 Error 的东西,您可以指定 error 字段的类型

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,
  }))
}

然而,这有一个缺点,即 injectQuery 所有其他泛型的类型推断将不再起作用。抛出非 Error 的对象通常不被认为是好的做法,因此如果您有一个子类,如 AxiosError,您可以使用类型细化来使 error 字段更具体。

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

注册全局错误类型

TanStack Query v5 允许通过修改 Register 接口来设置全局错误类型,而无需在调用端指定泛型。这将确保推断仍然有效,但 error 字段将是指定类型。如果您想强制调用端执行显式的类型细化,请将 defaultError 设置为 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
})

类型元数据

注册全局元数据

与注册 全局错误类型 类似,您还可以注册全局 Meta 类型。这确保了 查询变更 上的可选 meta 字段保持一致且类型安全。请注意,注册的类型必须扩展 Record<string, unknown>,以便 meta 仍然是一个对象。

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

类型化查询和变更键

注册查询和变更键类型

同样,与注册 全局错误类型 类似,您还可以注册全局 QueryKeyMutationKey 类型。这允许您为您的键提供与应用程序层级匹配的更多结构,并在库的所有表面区域上对它们进行类型化。请注意,注册的类型必须扩展 Array 类型,以便您的键保持为数组。

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

查询选项的类型定义

如果您将查询选项内联到 injectQuery 中,您将获得自动类型推断。但是,您可能希望将查询选项提取到单独的函数中,以便在 injectQuery 和例如 prefetchQuery 之间共享,或者在服务中管理它们。在这种情况下,您会失去类型推断。要恢复它,您可以使用 queryOptions 帮助函数。

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

此外,从 queryOptions 返回的 queryKey 知道与之关联的 queryFn,我们可以利用该类型信息使诸如 queryClient.getQueryData 之类的函数也了解这些类型。

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

如果没有 queryOptions,数据类型将是未知的,除非我们传递一个类型参数。

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

类型化变更选项

queryOptions 类似,您可以使用 mutationOptions 将变更选项提取到单独的函数中。

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)
      },
    })
  }
}

使用 skipToken 类型安全地禁用查询

如果您正在使用 TypeScript,您可以使用 skipToken 来禁用查询。当您想基于条件禁用查询,但仍希望查询保持类型安全时,这非常有用。有关更多信息,请阅读 禁用查询 指南。