정합성 보장 상태로 매칭

atHousingAnywhere의 전단 팀에서 2017년 말 이후 우리는 TypeScript를 사용하여 우리의 React와 노드 코드 라이브러리를 작성해 왔다.React에서 지원하는 응용 프로그램과 서버 측의 렌더링과 동적 내용 루트를 위한 노드 서비스가 있습니다.우리의 코드 라이브러리가 끊임없이 증가함에 따라 점점 더 많은 코드가 서로 다른 페이지와 모듈에 의해 중용됨에 따라 우리는 우리의 컴파일러를 지도하는 데 이익을 얻을 수 있기를 결정한다.그때부터 우리는 파일이나 모듈을 한 번에 옮겼다.우리의 대부분의 새 코드는 TypeScript로 작성되었고, 대부분의 코드 라이브러리도 이미 이전되었다.
그 이후로 우리는 코드가 정확하다는 것을 확보하기 위해 유형 시스템을 이용하는 방법을 찾고 있다.
우리가 깨달은 것은 우리가 더 이상 자바스크립트를 작성하지 않는다는 것이다.TypeScript는 다른 언어입니다.네, 언어의 대다수 의미는 같고 문법도 같습니다 (유형 포함하지 않음).이것은 JavaScript 초집합을 구축하는 목표이기 때문입니다.그러나 그것도 우리가 정확한 코드를 작성하는 것을 지도하고 도울 수 있는 컴파일러가 있다. 이것은 우리가 현재 유형을 사용하여 우리의 많은 문제를 모델링할 수 있다는 것을 의미한다.
이러한 인식은'Aha'의 순간이 아니라 우리가 겪은 일련의 변화와 우리가 채택한 모델로 인해 유형 시스템에서 더 많은 이익을 얻을 수 있다.나는 이 블로그에서 여러분과 그 중의 한 모델을 공유하고 싶습니다.

모델링 상태 시간


인코딩할 때, 우리는 응용 프로그램의 흐름을 제어하는 일부분으로 몇 개의 변체를 처리해야 한다.요청된 상태, 구성 요소의 상태, 보여야 할 내용, 또는 사용자가 보여야 할 항목을 선택할 수 있는 경우도 있습니다.
본고에서 나는 요청된 상태를 어떻게 모델링하는지 설명하고 최종적으로 사용자에게 표시할 결과를 결정할 것이다.여기에서 토론한 모델은 그 어떠한 다른 상황에도 응용할 수 있는데, 그 중에서 부울 값 하나만으로는 부족하다.더 전통적인 명령식 스타일부터 성명적인 스타일까지 다양한 방식으로 이 점을 실현하도록 안내하겠습니다. 이런 스타일은 유형 시스템을 이용하여 더욱 안전하게 합니다.
우리의 상태에서 우리는 네 가지 잠재적인 장면이 있는데 이를 LoadingStatus라고 부른다. 어떤 요청을 하기 전의 초기 장면이다.또 하나는 불러오고 있습니다. 요청이 충족되기를 기다리고 있습니다.마지막으로 성공하거나 잘못된 사례가 있다.
우리가 취할 수 있는 방법 중 하나는 볼 속성의 조합을 이용하여 그것들을 구분하는 것이다.이것은 이런 상태를 모델링하는 매우 흔히 볼 수 있는 방법으로 보통 사람들이 사용하는 기본 방법이다.우리의 첫 번째 판LoadingState은 다음과 같다.
interface LoadingState<T> {
  isLoading: boolean;
  isLoaded: boolean;
  error?: string;
  data?: T;
}
내가 전에 언급한 바와 같이 몇 가지 효과적인 조합이 가능하다.
const notAsked = {
  isLoading: false,
  isLoaded: false,
  error: undefined,
  data: undefined,
};
어떤 요청도 하기 전에
const loading = {
  isLoading: true,
  isLoaded: false,
  error: undefined,
  data: undefined,
};
요청이 끊어질 때.
const success = {
  isLoading: false,
  isLoaded: true,
  error: undefined,
  data: someData,
};
성공적인 솔루션🎉
const failure = {
  isLoading: false,
  isLoaded: true,
  error: "Fetch error",
  data: undefined,
};
실패한 솔루션😞
React 구성 요소에서 이 로드 상태를 사용하여 사용자에게 상태와 데이터를 표시할 수 있습니다.
const MyComponent = ({loadingState}) => {
  if (loadingState.isLoading) {
    return <Spinner />;
  }

  if (loadingState.error) {
    return <Alert type="danger">There was an error</Alert>;
  }

  if (loadingState.isLoaded) {
    return <Content data={loadingState.data} />;
  }

  return <EmptyContent />;
};
이것은 모든 유효한 상태에 적용됩니다.지금 우리 주가 이렇게 보인다면?사용자에게 무엇을 표시해야 합니까?
const what = {
  isLoading: true,
  isLoaded: true,
  error: "Fetch error",
  data: someData,
};
우리가 이런 상태에 들어간 것은 분명히 문제가 생겼다.한 가지 선택은'사실은 이렇다. 데이터가 일치하지 않을 수도 있다'는 것을 받아들이고 우리의 코드가 이런 일치하지 않는 상태에 들어가는 것을 방지하기 위해 테스트를 작성하는 것이다.또 다른 방법은 불일치 상태가 다른 오류라고 가정하고 코드를 작성하여 상태가 일치하지 않는지 확인하고 이런 상황이 발생할 때 사용자에게 오류를 표시하는 것이다.
근데 잠깐만.우리의 코드가 현재 정적 유형인 이상 이 문제를 해결하기 위해 무엇을 할 수 있습니까?
저는 일련의 개선을 겪고 잠재적이고 더욱 통용적인 개선을 보여 드리겠습니다. 마지막으로 제가 실제로 추천하는 버전입니다.면책 성명: 이런 모델은 흔하지 않습니다. 당신의 팀을 참여시키기 위해 약간의 노력이 필요할 수도 있습니다.즉, 나는 네가 그들이 별도의 노력을 기울일 가치가 있다는 것을 발견할 것이라고 믿는다. (내가 보기에, 어쨌든, 이것은 그렇게 많은 추가 노력이 없다.)
우리 시작합시다.

일치 사례: 하나의 속성이 모든 사례를 결정한다


첫 번째 상황에 대해 우리는 가장 통용되는 모델부터 시작할 것이다.모든 사례는 서로 배제되기 때문에 (예를 들어 데이터와 오류가 동시에 존재하지 말아야 하기 때문에) 정확한 방향의 한 단계는 모든 사례를 연합 유형으로 정의하는 것이다.
type Status =
  | "not_asked"
  | "loading"
  | "success"
  | "failure";

interface LoadingState<T> {
  status: Status;
  data?: T;
  error?: string;
}
현재 우리는 우리의 상태의 실제 원천으로 값을 가지고 있으며, 우리는 우리의 구성 요소를 업데이트할 수 있다.
const MyComponent = ({loadingState}) => {
  if (loadingState.status === "not_asked") {
    return <EmptyContent />;
  }

  if (loadingState.status === "loading") {
    return <Spinner />;
  }

  if (loadingState.status === "failure") {
    return (
      <Alert type="danger">
        {loadingState.error || "There was an error"}
      </Alert>
    );
  }

  if (laodingState.status === "success") {
    return loadingState.data ? (
      <Content data={loadingState.data} />
    ) : null;
  }
};
많이 좋아졌어요.우리는 (거의) 일치하지 않는 문제를 해결했다. 왜냐하면 우리의 상태는 현재 하나의 값으로만 정의되어 있기 때문이다.우리가 왜 내가 이 문제를 거의 해결했다고 말하는지 토론하기 전에, 우리 먼저 일을 정리해 봅시다.내가 시작할 때 언급한 목표 중 하나는 우리의 코드를 더욱 성명적으로 하는 것이다.그러나 정의에 따라 if 문장은 필수적이다.만약 우리가 한 걸음 물러서서 우리가 무엇을 하고 있는지 고려한다면, 우리는 특정한 상황을 처리할 수 있도록 어떤 방식으로 모든 변체를 일치시키려고 시도할 것이다.이것은 매우 간단하고 간단하지만 기능이 강한 실용적인 기능으로 바뀔 수 있다.
type Matcher<Keys extends string, R> = {
  [K in Keys]: (k: K) => R;
};

const match = <Keys extends string, R = void>(
  m: Matcher<Keys, R>,
) => (k: Keys) => m[k](k);
만약 우리가 형식을 삭제한다면, 나머지 함수는 하나의 대상을 받아들이고 문자열을 받아들이는 함수를 되돌려주고, 이 문자열을 사용하여 대상의 속성을 찾고 문자열로 호출합니다.네, 코드보다 영어에서 더 길어요.
const match = (m) => (k) => m[t](k);

match({foo: () => "bar"})("foo"); // => 'bar'
실현은 간단하고 말이 필요 없으니 유형을 봅시다.우리는 대상의 키를 유형 매개 변수로 제공합니다.문자열을 확장해야 합니다. 이것은 열거나 문자열의 연합 형식일 수 있음을 의미합니다.이것은 대상이 모든 키를 정의하고 exhaustiveness check 를 제공해야 한다는 것을 보장합니다. 즉, 속성 중 하나가 부족하면 컴파일러가 오류를 낼 수 있습니다.
이제 코드를 다시 업데이트할 수 있습니다.
const MyComponent = ({loadingState}) => (
  match < Status,
  React.ReactNode >
    {
      not_asked: () => <EmptyContent />,
      loading: () => <Spinner />,
      success: () =>
        loadingState.data ? (
          <Content data={loadingState.data} />
        ) : null,
      failure: () => (
        <Alert type="danger">
          {loadingState.error || "There was an error"}
        </Alert>
      ),
    }(loadingState.status)
);
이것은 일을 더욱 성명성을 가지게 한다.많이 좋아졌어!만약 우리가 그 중의 한 가지 상황을 처리하는 것을 잊어버린다면, 컴파일러는 우리에게 매우 편리하다는 것을 일깨워 줄 것이다.

모든 사례가 되돌아와야 할 내용을 명확하게 정의하는 것이 아니라 코드를 어떻게 확정하여 더욱 성명성을 가지게 해야 하는지를 표현하는 것 외에 우리는 코드를 장래에 더욱 쉽게 업데이트할 수 있도록 한다.우리가 당신의 연합 유형에 더 많은 사례를 추가함에 따라, 우리는 업데이트가 필요한 코드를 여기저기 검색할 필요가 없습니다.컴파일러는 모든 상황을 포괄할 수 있도록 우리에게 통지할 뿐이다.
Housing Anywhere에서, 우리는 이러한 모델을 매우 좋아한다. 이를React 구성 요소에 사용할 뿐만 아니라, 기본적으로 코드의 어느 곳에서든(reducer,thunk,services 등) 사용한다.비록 이것은 간단하고 짧은 모듈이지만, 우리는 이미 그것을 하나의 패키지로 제공했다: @housinganywhere/match.

미세 조정: 마지막 일치


초기 방법에 비해 우리는 이미 매우 큰 개선을 했고, 빈거성 검사로 인해 그것의 확장성이 매우 좋다.
그러나 앞에서 보듯이 성공과 실패 사례에 정의되지 않은 값이 있는지 확인해야 합니다.우리는 왜 이런 상황을 피해야 합니까?우리는 여전히 무의미한 불일치 상태로 끝날 기회가 있기 때문이다.
const leWhat = {
  status: "not_asked",
  data: someData,
  error: "Oops this makes no sense!",
};

const queEsEsto = {
  status: "success",
  data: undefined,
  error: undefined,
};
우리가 진정으로 원하는 것은 컴파일러가 우리가 일치하는 상태만 있을 수 있다는 것을 보증하는 것이다.앞에서 말한 바와 같이, 우리가 사용LoadingState할 때, 우리는 기본적으로 모든 사례에 일치하여 그것들을 처리한다.왠지 모르게 우리는 우리의 지위와 일치해야 할 뿐만 아니라 우리 나라의 전체 형상과 일치해야 한다.
입력discriminated unions(또는 tagged unions라고도 함).
판별 병집은 모든 사례의 병집으로 구성되는데 그 중에서 모든 사례는 하나의 공공적인 단례 유형 속성, 즉 판별식이나 표기를 가지고 있어야 한다.algebraic data types 검사 코드의 판별식을 사용하면 컴파일러는 우리가 일치하는 상황을 이해할 수 있습니다.
우리 LoadingState 를 차별받는 연맹으로 다시 정의합시다.
type LoadingState<T> =
  | {status: "not_asked"}
  | {status: "loading"}
  | {status: "success"; data: T}
  | {status: "failure"; error: string};
현재, 우리는 match의 한 버전을 실현할 수 있다. 이 버전은 전문적으로 LoadingState의 이 버전을 맞춤형으로 제작할 수 있다.
type LoadingStateMatcher<Data, R> = {
  not_asked: () => R;
  loading: () => R;
  success: (data: Data) => R;
  failure: (err: string) => R;
};

const match = <Data, R = void>(
  m: LoadingStateMatcher<Data, R>,
) => (ls: LoadingState<Data>) => {
  if (ls.status === "not_asked") {
    return m.not_asked();
  }

  if (ls.status === "loading") {
    return m.loading();
  }

  if (ls.status === "success") {
    return m.success(ls.data);
  }

  return m.failure(ls.error);
};
마찬가지로, 우리는 상태에 대한 모든 상황을 포함하는 matcher 대상을 정의했다.그러나 이번에는 방법마다 다른 서명이 있고 호출해야 할 때 데이터나 오류로 호출됩니다.판별식 (status 속성) 을 검사하면 컴파일러가 우리가 처한 상황을 이해할 수 있습니다.
type guards
컴파일러는 상태가 "성공"일 때 데이터를 정의했다는 것을 안다
우리는 match 유틸리티의 최종 버전을 사용하여 사용 상황을 다시 업데이트할 것이다.
const MyComponent = ({loadingState}) =>
  match<SomeData, React.ReactNode>({
    not_asked: () => <EmptyContent />,
    loading: () => <Spinner />,
    success: (data) => <Content data={data} />,
    failure: (err) => <Alert type="danger">{err}</Alert>,
  })(loadingState.status);
우리는 상태에 대한 처리를 더욱 성명성과 간결성을 가지게 할 뿐만 아니라, 더욱 안전하게 한다. 왜냐하면 컴파일러가 우리가 무엇을 잘못했는지 가볍게 알려주지 않으면 이상한 불일치 상태에 들어갈 수 없기 때문이다.
비록 이런 좋은 점은 보기에는 그리 대단한 것이 아닐 수도 있지만, 그것들은 상당히 중요하다.유형 시스템의 모든 요점은 코드에서 오류가 발생했을 때 다른 오류 원본을 보지 말라는 것이다.반대로 그 기능은 우리가 더욱 안전한 코드를 작성하는 데 도움을 주는 것이다. 방법은 코드를 모델링하여 우리가 컴파일할 때 코드가 정확한지 식별할 수 있도록 하는 것이다.

한층 더 읽다


이 모드를 사용하여 다른 문제를 시뮬레이션하려면 다음 두 가지 예를 참조하십시오.
내가 여기서 제기한 생각은 새로운 것도 아니고 TypeScript만의 것도 아니다.더 많은 정보를 얻으려면 패턴 매칭 (즉 매칭이 다른 연합과 데이터 형태) 이 어떻게 다른 언어에서 본 컴퓨터에서 이루어지는지, 그리고 유형 구동 개발을 이용하여 불일치 상태에 있을 수 없는 코드를 작성하는지 보십시오.

  • Reasonml: Pattern matching
  • How Elm Slays a UI Antipattern
  • 즐겁고 안전한 인코딩!🎉

    좋은 웹페이지 즐겨찾기