löfte - Hur man använder fetch i TypeScript


Jag använder window.fetch i Typescript, men jag kan inte kasta svaret direkt till min anpassade typ:

Jag kringgår detta genom att kasta Promise-resultatet till en mellanliggande 'any'-variabel.

Vad skulle vara det korrekta sättet att göra detta?

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

Svar #1

Vänta på version

Tyvärr accepterar varken fetch eller response som den returnerar generisk input, så vi behöver göra lite typomvandling (ses som as T nedan).

Det är dock vanligt att kapsla in fetch i ditt eget projekt för att tillhandahålla eventuella vanliga headers, en grundläggande URL osv., och så att du kan tillåta generisk input inom din applikation.

Exempel på att omsluta fetch och låta applikationen definiera formen på varje API-svar.

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

Lekplats

Tidigare versioner

Notera: De tidigare versionerna använde promises vid skrivandet, innan await/async blev vanligt förekommande i webbläsaren. De är fortfarande användbara ur ett lärandeperspektiv, och därför behålls de.

Löfteversion

Det har skett några förändringar sedan detta svar skrevs för ett tag sedan. Som nämnts i kommentarerna är response.json<T> inte längre giltig. Inte säker, kunde inte hitta var den togs bort.

För senare versioner kan du göra:

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

Gammalt svar

Några exempel följer, från grundläggande till att lägga till transformationer efter förfrågan och/eller felhantering:

Grundläggande :

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

Datatransformationer:

Ofta kan du behöva göra vissa justeringar av datan innan den skickas till konsumenten, till exempel att packa upp ett toppnivådataattribut. Detta är enkelt:

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

Felhanteirng:

Jag skulle hävda att du inte direkt bör fånga fel inom denna tjänst, istället bör du låta det bubbla upp, men om du behöver, kan du göra följande:

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

Svar #2

Faktiskt, nästan var som helst i TypeScript kommer det att fungera som önskat att skicka ett värde till en funktion med en specificerad typ så länge typen som skickas är kompatibel.

Det sagt fungerar följande...

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

Jag ville samla alla mina HTTP-anrop i en återanvändbar klass - vilket betyder att jag behövde ett sätt för klienten att bearbeta svaret i önskat format. För att stödja detta accepterar jag en callback-lambda som en parameter till min wrapper-metod. Lambda-deklarationen accepterar en typ som vilken som helst som visas här...

callBack: (response: any) => void

Men vid användning kan anroparen skicka en lambda som anger den önskade returtypen. Jag ändrade min kod ovanifrån så här...

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

Så att en klient kan kalla på det med en återuppringning som...

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

Relaterade artiklar