(저희 팀과 함께) TypeScript 및 Svelte(우리 모두가 싫어하는 JavaScript 및 React)로 애플리케이션을 다시 작성하기 시작했을 때 다음과 같은 문제에 직면했습니다.
HTTP 응답의 가능한 모든 본문을 안전하게 입력하려면 어떻게 해야 하나요?
이것이 당신에게 종소리를 울리나요? 그렇지 않다면 아마도 당신은 "그 중 하나"일 것입니다. 헤헤. 그림을 더 잘 이해하기 위해 잠시 다른 이야기를 해보자.
아무도 HTTP 응답의 "가능한 모든 본문"에 관심을 두지 않는 것 같습니다. 이미 HTTP 응답을 위해 만들어진 항목을 찾을 수 없었기 때문입니다(아마도 ts-fetch). 이것이 왜 그런지에 대한 논리를 빠르게 살펴보겠습니다.
사람들이 다음과 같은 이유로 아무도 신경 쓰지 않습니다.
행복한 경로만 고려하세요. HTTP 상태 코드가 2xx일 때의 응답 본문입니다.
사람들이 다른 곳에서 직접 입력합니다.
#1의 경우, 개발자(특히 경험이 없는 개발자)는 HTTP 요청이 실패할 수 있으며 실패한 응답에 전달된 정보가 일반 응답과 완전히 다를 가능성이 높다는 사실을 망각하고 있다고 말하고 싶습니다.
#2에서는 ky 및 axios와 같은 인기 있는 NPM 패키지에서 발견된 큰 문제를 살펴보겠습니다.
내가 아는 한 사람들은 ky 또는 axios와 같은 패키지를 좋아합니다. 그 이유 중 하나가 OK가 아닌 HTTP 상태 코드에 오류를 발생시킨다는 것입니다. 언제부터 괜찮았어? 이후로. 그러나 분명히 사람들은 이것을 받아들이지 않습니다. 사람들은 만족스럽지 않고 OK가 아닌 응답에 오류가 발생하는 것에 만족합니다.
잡을 시간이 되면 사람들이 OK가 아닌 신체를 입력한다고 상상해 봅니다. 정말 엉망이고 코드 냄새가 나요!
try..catch 블록을 분기 문으로 효과적으로 사용하고 있지만 try..catch는 분기 문이 아니기 때문에 이는 코드 냄새입니다.
그러나 분기가 try..catch에서 자연스럽게 발생한다고 주장하더라도 이것이 여전히 나쁜 또 다른 큰 이유가 있습니다. 오류가 발생하면 런타임에서 호출 스택을 해제해야 합니다. 이는 if 또는 switch 문을 사용한 일반 분기보다 CPU 주기 측면에서 훨씬 더 비용이 많이 듭니다.
이 사실을 알면 단지 try..catch 블록을 오용하여 성능 저하를 정당화할 수 있습니까? 나는 아니오라고 말한다. 프론트엔드 세계가 이것에 완전히 만족하는 이유는 단 한 가지도 없습니다.
이제 제가 추론한 내용을 설명했으니 본론으로 돌아가겠습니다.
HTTP 응답은 상태 코드에 따라 다른 정보를 전달할 수 있습니다. 예를 들어, PATCH HTTP 요청을 수신하는 api/todos/:id와 같은 todo 엔드포인트는 응답 상태 코드가 400일 때와 응답 상태 코드가 200일 때 다른 본문으로 응답을 반환할 수 있습니다.
예를 들어 보겠습니다.
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
이를 염두에 두고 문제 설명으로 돌아가겠습니다. 작성 중인 HTTP 상태 코드에 따라 TypeScript가 내가 다루고 있는 본문을 알려줄 수 있는 이 PATCH 요청을 수행하는 함수를 어떻게 입력할 수 있습니까? 암호? 대답: 유창한 구문(빌더 구문, 연결 구문)을 사용하여 유형을 축적합니다.
이전 유형을 기반으로 하는 유형을 정의하는 것부터 시작하겠습니다.
export type AccumType<T, NewT> = T | NewT;
매우 간단합니다. T와 NewT 유형이 주어지면 이를 결합하여 새로운 유형을 형성합니다. 이 새로운 유형을 AccumType<>에서 다시 T로 사용하면 또 다른 새로운 유형을 축적할 수 있습니다. 그러나 이 작업을 손으로 수행하는 것은 좋지 않습니다. 솔루션의 또 다른 핵심 요소인 Fluent 구문을 소개하겠습니다.
메서드가 항상 자신(또는 자신의 복사본)을 반환하는 클래스 X의 개체가 있는 경우 메서드 호출을 차례로 연결할 수 있습니다. 이는 유창한 구문 또는 연결 구문입니다.
이를 수행하는 간단한 클래스를 작성해 보겠습니다.
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
유레카! 유창한 구문과 우리가 작성한 유형을 성공적으로 결합하여 데이터 유형을 단일 유형으로 축적하기 시작했습니다!
명확하지 않은 경우 원하는 유형이 누적될 때까지(x.accumulate().accumulate()… 완료될 때까지) 운동을 계속할 수 있습니다.
다 좋은데 이 초간단 유형은 HTTP 상태 코드를 해당 본문 유형에 묶지 않습니다.
우리가 원하는 것은 유형 축소 기능이 시작될 수 있도록 TypeScript에 충분한 정보를 제공하는 것입니다. 이를 위해 원래 문제와 관련된 코드를 얻는 데 필요한 작업을 수행해 보겠습니다(HTTP 응답 본문을 -상태 코드 기준).
먼저 AccumType의 이름을 바꾸고 진화시킵니다. 아래 코드는 반복 진행 과정을 보여줍니다.
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
이 시점에서 저는 다음과 같은 사실을 깨달았습니다. 상태 코드는 유한합니다. 상태 코드를 찾아 유형을 정의하고 해당 유형을 사용하여 유형 매개변수 TStatus를 제한할 수 있다는 것입니다.
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
우리는 정말 아름다운 일련의 유형에 도달했습니다. ok 또는 status 속성의 조건을 기반으로 분기(if 문 작성)함으로써 TypeScript의 유형 축소 기능이 시작됩니다! 믿을 수 없다면 수업 부분을 작성하고 시도해 보세요.
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
시험해 보세요:
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
이제 상태 속성의 ok 속성을 기반으로 분기 시 유형 축소가 몸체의 모양을 올바르게 예측할 수 있는 이유가 명확해졌습니다.
그러나 문제가 있습니다. 클래스가 인스턴스화될 때 클래스의 초기 유형이 위의 주석 블록에 표시되어 있습니다. 저는 이렇게 해결했습니다:
export type AccumType<T, NewT> = T | NewT;
이 작은 변화로 초기 타이핑이 사실상 제외되어 이제 영업을 시작하게 되었습니다!
이제 다음과 같은 코드를 작성할 수 있으며 Intellisense는 100% 정확합니다.
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
ok 속성을 쿼리할 때도 유형 축소가 작동합니다.
당신이 눈치 채지 못했다면, 우리는 오류를 던지지 않음으로써 훨씬 더 나은 코드를 작성할 수 있었습니다. 내 전문적인 경험에 따르면 axios도 틀리고, ky도 틀리고, 같은 작업을 수행하는 다른 가져오기 도우미도 틀립니다.
TypeScript는 정말 재미있습니다. TypeScript와 유창한 구문을 결합함으로써 우리는 필요한 만큼 많은 유형을 축적할 수 있으므로 반복해서 디버깅한 후가 아니라 첫날부터 더 정확하고 명확한 코드를 작성할 수 있습니다. 이 기술은 성공적인 것으로 입증되었으며 누구나 시도해 볼 수 있습니다. dr-fetch를 설치하고 테스트해 보세요.
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
또한 더 이상 사용되지 않는 .env 파일과 dotenv를 완전히 제거하는 것을 목표로 하는 패키지인 wj-config도 만들었습니다. 이 패키지는 여기서 설명하는 TypeScript 트릭도 사용하지만 유형을 |가 아닌 &로 결합합니다. 한번 사용해 보고 싶다면 v3.0.0-beta.1을 설치하세요. 그러나 타이핑은 훨씬 더 복잡합니다. wj-config 이후 dr-fetch를 만드는 것은 공원 산책이었습니다.
가져오기 관련 패키지에 있는 몇 가지 오류를 살펴보겠습니다.
README에서 다음 내용을 확인할 수 있습니다.
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
“서버 응답이 좋지 않습니다”?? 아니요. "서버가 귀하의 요청이 잘못되었다고 말합니다." 네, 던지는 부분 자체가 형편없습니다.
이 아이디어는 맞지만 안타깝게도 OK와 OK가 아닌 응답만 입력할 수 있습니다(최대 2가지 유형).
제가 가장 비판한 패키지 중 하나는 다음 예를 보여줍니다.
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
이것은 아주 후배 개발자가 쓸 내용입니다. 바로 행복한 길입니다. README에 따른 동등성:
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
던지는 부분이 너무 안 좋아요. 왜 던지기 위해 가지를 치고, 나중에 잡으라고 강요하나요? 그것은 나에게 전혀 의미가 없습니다. 오류의 텍스트도 오해의 소지가 있습니다. 이는 "가져오기 오류"가 아닙니다. 가져오기가 작동했습니다. 답장 받았지, 그렇지? 당신은 단지 그것이 마음에 들지 않았을 뿐입니다… 왜냐하면 그것은 행복한 길이 아니기 때문입니다. 더 나은 표현은 "HTTP 요청 실패:"입니다. 실패한 것은 가져오기 작업이 아니라 요청 자체였습니다.
위 내용은 TypeScript를 사용하여 유형을 누적하는 방법: 가능한 모든 fetch() 결과 입력의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!