如何在打字稿中使用获取

发布于 2025-02-03 20:33:16 字数 631 浏览 3 评论 0 原文

我正在使用 window> window.fetch..fetch 在Typescript中直接进入我的自定义类型:

我通过将承诺结果扔到中间的“任何”变量来解决这一问题。

做到这一点的正确方法是什么?

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then(res => {
          // this is not allowed
          // let a:Actor = <Actor>res;

          // I use an intermediate variable a to get around this...
          let a:any = res; 
          let b:Actor = <Actor>a;
      })

I am using window.fetch in Typescript, but I cannot cast the response directly to my custom type:

I am hacking my way around this by casting the Promise result to an intermediate 'any' variable.

What would be the correct method to do this?

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then(res => {
          // this is not allowed
          // let a:Actor = <Actor>res;

          // I use an intermediate variable a to get around this...
          let a:any = res; 
          let b:Actor = <Actor>a;
      })

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

绾颜 2025-02-10 20:33:16

不幸的是

,不幸的是, fet> fetch 也不是响应它返回接受通用输入,因此我们需要进行某种类型的铸造(将其视为为t 以下)。

但是,通常会在您自己的项目中获取以提供任何通用标题,基本URL等,以便您可以在应用程序中允许通用输入。

包装摘要的示例,并允许应用程序定义每个API响应的形状。

const BASE_URL = 'http://example.org';

//         Input T ↴   is thread through to ↴
async function api<T>(path: string): Promise<T> {
    const response = await fetch(`${BASE_URL}/${path}`);

    if (!response.ok) {
      throw new Error(response.statusText);
    }

    //    And can also be used here ↴
    return await response.json() as T;
}

// Set up various fetches
async function getConfig() {
  //             Passed to T ↴
  return await api<{ version: number }>('config');
}

// Elsewhere
async function main() {
  const config = await getConfig();

  // At this point we can confidently say config has a .version
  // of type number because we threaded the shape of config into 
  // api() 
  console.log(config.version); 
}

Playground

Previous versions

Note :以前的版本在撰写本文时使用了承诺,在浏览器中通常会发现/异步。从学习的角度来看,它们仍然很有用,因此保留它们。

承诺版本

自一段时间以来编写此答案以来发生了一些更改。如评论中所述, wenders.json&lt; t&gt; 不再有效。不确定,找不到将其删除的地方。

对于以后的发行版,您可以执行:

// Standard variation
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<T>
    })
}


// For the "unwrapping" variation

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<{ data: T }>
    })
    .then(data => {
        return data.data
    })
}

旧答案

以下几个示例,从基本到请求和/或错误处理后添加转换:

基本:

// Implementation code where T is the returned data shape
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<T>()
    })

}

// Consumer
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

数据转换:

通常您可能需要对数据进行一些调整,然后例如,对于消费者而言,取消顶级数据属性。这是直接的:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => { /* <-- data inferred as { data: T }*/
      return data.data
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

错误处理:

我认为您不应该直接在此服务中直接捕获错误,而只是允许它起泡,但是如果需要,您可以执行以下操作:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => {
      return data.data
    })
    .catch((error: Error) => {
      externalErrorLogging.error(error) /* <-- made up logging service */
      throw error /* <-- rethrow the error so consumer can still catch it */
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Await version

Unfortunately, neither fetch, nor the response it returns accept generic input, so we need to do some type casting (seen as as T below).

However, it is common to wrap fetch in your own project to provide any common headers, a base url, etc, and so you can allow generic input within your application.

Example of wrapping fetch, and allowing the application to define the shape of each api response.

const BASE_URL = 'http://example.org';

//         Input T ↴   is thread through to ↴
async function api<T>(path: string): Promise<T> {
    const response = await fetch(`${BASE_URL}/${path}`);

    if (!response.ok) {
      throw new Error(response.statusText);
    }

    //    And can also be used here ↴
    return await response.json() as T;
}

// Set up various fetches
async function getConfig() {
  //             Passed to T ↴
  return await api<{ version: number }>('config');
}

// Elsewhere
async function main() {
  const config = await getConfig();

  // At this point we can confidently say config has a .version
  // of type number because we threaded the shape of config into 
  // api() 
  console.log(config.version); 
}

Playground

Previous versions

Note: The previous versions used promises at the time of writing, prior to await/async being commonly found in the browser. They are still useful from a learning perspective, and so they are kept.

Promise version

There has been some changes since writing this answer a while ago. As mentioned in the comments, response.json<T> is no longer valid. Not sure, couldn't find where it was removed.

For later releases, you can do:

// Standard variation
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<T>
    })
}


// For the "unwrapping" variation

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json() as Promise<{ data: T }>
    })
    .then(data => {
        return data.data
    })
}

Old Answer

A few examples follow, going from basic through to adding transformations after the request and/or error handling:

Basic:

// Implementation code where T is the returned data shape
function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<T>()
    })

}

// Consumer
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Data transformations:

Often you may need to do some tweaks to the data before its passed to the consumer, for example, unwrapping a top level data attribute. This is straight forward:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => { /* <-- data inferred as { data: T }*/
      return data.data
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })

Error handling:

I'd argue that you shouldn't be directly error catching directly within this service, instead, just allowing it to bubble, but if you need to, you can do the following:

function api<T>(url: string): Promise<T> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw new Error(response.statusText)
      }
      return response.json<{ data: T }>()
    })
    .then(data => {
      return data.data
    })
    .catch((error: Error) => {
      externalErrorLogging.error(error) /* <-- made up logging service */
      throw error /* <-- rethrow the error so consumer can still catch it */
    })
}

// Consumer - consumer remains the same
api<{ title: string; message: string }>('v1/posts/1')
  .then(({ title, message }) => {
    console.log(title, message)
  })
  .catch(error => {
    /* show error message */
  })
喜爱纠缠 2025-02-10 20:33:16

实际上,只要传递的类型兼容,就几乎在打字稿中的任何地方,都将值传递给具有指定类型的函数。

话虽如此,以下工作...

 fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then((res: Actor) => {
          // res is now an Actor
      });

我想将所有HTTP调用包装在可重复使用的类中 - 这意味着我需要某种方法让客户以所需的形式处理响应。为了支持这一点,我接受回调lambda作为我的包装方法的参数。 Lambda声明接受此处所示的任何类型...

callBack: (response: any) => void

但是在使用中,呼叫者可以通过指定所需返回类型的lambda。我像这样修改了上面的代码...

fetch(`http://swapi.co/api/people/1/`)
  .then(res => res.json())
  .then(res => {
      if (callback) {
        callback(res);    // Client receives the response as desired type.  
      }
  });

以便客户可以用像...的回调调用它。

(response: IApigeeResponse) => {
    // Process response as an IApigeeResponse
}

Actually, pretty much anywhere in typescript, passing a value to a function with a specified type will work as desired as long as the type being passed is compatible.

That being said, the following works...

 fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json())
      .then((res: Actor) => {
          // res is now an Actor
      });

I wanted to wrap all of my http calls in a reusable class - which means I needed some way for the client to process the response in its desired form. To support this, I accept a callback lambda as a parameter to my wrapper method. The lambda declaration accepts an any type as shown here...

callBack: (response: any) => void

But in use the caller can pass a lambda that specifies the desired return type. I modified my code from above like this...

fetch(`http://swapi.co/api/people/1/`)
  .then(res => res.json())
  .then(res => {
      if (callback) {
        callback(res);    // Client receives the response as desired type.  
      }
  });

So that a client can call it with a callback like...

(response: IApigeeResponse) => {
    // Process response as an IApigeeResponse
}
耳根太软 2025-02-10 20:33:16

如果您看一下您将看到身体的定义

export class Body {
    bodyUsed: boolean;
    body: NodeJS.ReadableStream;
    json(): Promise<any>;
    json<T>(): Promise<T>;
    text(): Promise<string>;
    buffer(): Promise<Buffer>;
}

,这意味着您可以使用仿制药来实现所需的目标。我没有测试此代码,但看起来像这样:

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json<Actor>())
      .then(res => {
          let b:Actor = res;
      });

If you take a look at @types/node-fetch you will see the body definition

export class Body {
    bodyUsed: boolean;
    body: NodeJS.ReadableStream;
    json(): Promise<any>;
    json<T>(): Promise<T>;
    text(): Promise<string>;
    buffer(): Promise<Buffer>;
}

That means that you could use generics in order to achieve what you want. I didn't test this code, but it would looks something like this:

import { Actor } from './models/actor';

fetch(`http://swapi.co/api/people/1/`)
      .then(res => res.json<Actor>())
      .then(res => {
          let b:Actor = res;
      });
撩发小公举 2025-02-10 20:33:16

这是专门为发布请求而编写的。这就是为什么它具有“变量”参数。如果“获取”请求相同的代码将起作用,则可以选择可选

export type FetcherOptions = {
  queryString: string
  variables?: FetcherVariables
}

export type FetcherVariables = {[key: string]: string | any | undefined}

export type FetcherResults<T> = {
  data: T
}

const fetcher = async <T>({queryString, 
                           variables }: FetcherOptions): Promise<FetcherResults<T>> => {
  const res = await fetch(API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // You can add more headers
    },
    body: JSON.stringify({
      queryString,
      variables
    })
  })
  const { data, errors} = await res.json()

  if (errors) {
    // if errors.message null or undefined returns the custom error
    throw new Error(errors.message ?? "Custom Error" )
  }

  return { data }
}

This is specifically written for POST request. That is why it has "variables" parameter. In case of "GET" request same code will work, vriables can be optional is handled

export type FetcherOptions = {
  queryString: string
  variables?: FetcherVariables
}

export type FetcherVariables = {[key: string]: string | any | undefined}

export type FetcherResults<T> = {
  data: T
}

const fetcher = async <T>({queryString, 
                           variables }: FetcherOptions): Promise<FetcherResults<T>> => {
  const res = await fetch(API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // You can add more headers
    },
    body: JSON.stringify({
      queryString,
      variables
    })
  })
  const { data, errors} = await res.json()

  if (errors) {
    // if errors.message null or undefined returns the custom error
    throw new Error(errors.message ?? "Custom Error" )
  }

  return { data }
}
雾里花 2025-02-10 20:33:16

对于此特定的用例:

“从远程资源获取数据,我们没有控制权,并且想要在当前应用程序中注入过滤器


https://www.npmjs.com/package/package/zod

以下时尚:

// 1. Define a schema

const Data = z.object({
  // subset of real full type
  name: z.string(),
  // unExpectedAttr: z.number(), --> enabling this will throw ZodError 
  height: z.string(),
  mass: z.string(),
  films: z.array(z.string()),
});

// 2. Infer a type from the schema to annotate the final obj

type DataType = z.infer<typeof Data>;

(async () => {
  try {
    const r = await fetch(`https://swapi.dev/api/people/1/?format=json`);
    const obj: DataType = Data.parse(await r.json());
    console.log(obj); // filtered with expected field in Data Schema
    /**
     Will log:
     {
       name: 'Luke Skywalker',
       height: '172',
       mass: '77',
       films: [
        'https://swapi.dev/api/films/1/',
        'https://swapi.dev/api/films/2/',
        'https://swapi.dev/api/films/3/',
        'https://swapi.dev/api/films/6/'
       ]
     }
    */

  } catch (error) {
    if (error instanceof ZodError) {
      // Unexpected type in response not matching Data Schema
    } else {
      // general unexpected error
    }
  }
})();

For this particular use-case:

"Fetching data from a remote resource, we do not have control and want to validate filter before injecting in our current application"

I feel recommending zod npm package
https://www.npmjs.com/package/zod

with the following fashion:

// 1. Define a schema

const Data = z.object({
  // subset of real full type
  name: z.string(),
  // unExpectedAttr: z.number(), --> enabling this will throw ZodError 
  height: z.string(),
  mass: z.string(),
  films: z.array(z.string()),
});

// 2. Infer a type from the schema to annotate the final obj

type DataType = z.infer<typeof Data>;

(async () => {
  try {
    const r = await fetch(`https://swapi.dev/api/people/1/?format=json`);
    const obj: DataType = Data.parse(await r.json());
    console.log(obj); // filtered with expected field in Data Schema
    /**
     Will log:
     {
       name: 'Luke Skywalker',
       height: '172',
       mass: '77',
       films: [
        'https://swapi.dev/api/films/1/',
        'https://swapi.dev/api/films/2/',
        'https://swapi.dev/api/films/3/',
        'https://swapi.dev/api/films/6/'
       ]
     }
    */

  } catch (error) {
    if (error instanceof ZodError) {
      // Unexpected type in response not matching Data Schema
    } else {
      // general unexpected error
    }
  }
})();

请别遗忘我 2025-02-10 20:33:16

新解决方案(2024-12)

当我自己在工作中面临问题时,我对这个话题感兴趣的 。最受欢迎的答案对我来说是一个很大的否定,因为:

  1. 它仅键入快乐路径: wenders.ok true
  2. 非OK响应上丢弃错误,该错误会促进“遵循快乐的道路”代码气味

另外,为什么如果(response.ok)提出错误?这迫使您在某处添加 try..catch 。只需测试状态代码或 ok 值即可。投掷是一种可怕的练习。

无论如何,我需要键入所有可能的身体:例如,我的API在400个不良请求响应中返回一个有意义的身体。

没有什么可以解决 n-types-for-n-status-codes 问题,所以我做了自己的:npm软件包 dr-fetch 通过使用链语法累积类型来解决此问题。

首先,创建Fetcher对象,该对象允许自定义的身体解析器和自定义的数据提取功能。前者是用 content-type 的响应来处理不常见的响应;后者是进行前/提取工作:

// myFetch.ts
import { obtainToken } from './magical-auth-stuff.js';
import { setHeaders } from 'dr-fetch';

export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
    const token = obtainToken();
    init ??= {};
    setHeaders(init, { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`});
    return fetch(url, init);
}

// fetcher.ts
import { DrFetch } from "dr-fetch";
import { myFetch } from "./myFetch.js";

export default new DrFetch(myFetch);
// If you don't need a custom fetch function, just do:
export default new DrFetch();

现在我们在需要的地方导入此fetcher对象,为URL键入并获取:

import fetcher from './fetcher.js';
import type { ServerErrorStatusCode } from 'dr-fetch';

const response = await fetcher
    .for<200, MySuccessType>()
    .for<400, MyBadRequestType>()
    .for<500, MyServerErrorType>()
    // OR, with a helper type to cover more status codes at once:
    .for<ServerErrorStatusCode, MyServerErrorType>()
    .post('the/url', { my: body })
    ;

// At this point, TypeScript can type-narrow 'response.body' using 
// 'response.status' or 'response.ok':
if (response.status === 501) {
    // response.body is of type MyServerErrorType
}
else if (response.status === 200) {
    // response.body is of type MySuccessType
}

看到吗?这是非常清楚,简洁的代码,不需要 try..catch 非OK响应。停止疯狂:拒绝进行非OK回应。

Newer Solution Available (2024-12)

I got interested in this topic when I was faced with the problem myself at work. The most accepted answer is a big NO to me because:

  1. It only types the happy path: When response.ok is true
  2. Throws errors on non-ok responses, which promotes the "follow the happy path" code smell

Also, why do an if (response.ok) to throw an error? This forces you to add a try..catch somewhere. Just test for the status code or the ok value. Throwing is a terrible, terrible practice.

Anyway, I needed to type all possible bodies: For example, my API returns a meaningful body on 400 BAD REQUEST responses.

There was nothing out there that solves the N-types-for-N-status-codes problem, so I made my own: The NPM package dr-fetch resolves this by accumulating types using chain syntax.

First, create the fetcher object, which allows for custom body parsers and customized data-fetching functions. The former is to handle responses with content-type's that are uncommon; the latter is to do pre/post fetch work:

// myFetch.ts
import { obtainToken } from './magical-auth-stuff.js';
import { setHeaders } from 'dr-fetch';

export function myFetch(url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) {
    const token = obtainToken();
    init ??= {};
    setHeaders(init, { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`});
    return fetch(url, init);
}

// fetcher.ts
import { DrFetch } from "dr-fetch";
import { myFetch } from "./myFetch.js";

export default new DrFetch(myFetch);
// If you don't need a custom fetch function, just do:
export default new DrFetch();

Now we import this fetcher object where needed, type it for the URL, and fetch:

import fetcher from './fetcher.js';
import type { ServerErrorStatusCode } from 'dr-fetch';

const response = await fetcher
    .for<200, MySuccessType>()
    .for<400, MyBadRequestType>()
    .for<500, MyServerErrorType>()
    // OR, with a helper type to cover more status codes at once:
    .for<ServerErrorStatusCode, MyServerErrorType>()
    .post('the/url', { my: body })
    ;

// At this point, TypeScript can type-narrow 'response.body' using 
// 'response.status' or 'response.ok':
if (response.status === 501) {
    // response.body is of type MyServerErrorType
}
else if (response.status === 200) {
    // response.body is of type MySuccessType
}

See? This is very clear, concise code that doesn't require try..catch for non-OK responses. Stop the madness: Say no to throwing on non-OK responses.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文