TypeScript를 사용하여 Object의 유형 보호 함수를 시각적으로 정의할 수 있는 라이브러리를 만들었습니다.

51067 단어 TypeScriptnpmtech

개시하다


typescanner라는 Object의 보호 함수를 직관적으로 정의할 수 있는 프로그램 라이브러리를 만들었습니다.
자세한 사용 방법은 README입니다.MD에 썼기 때문에 이 글에서 간단하게 소개하고 싶습니다.
https://github.com/yona3/typescanner

Example


처음엔 형방 설명을 썼더니 좀 길었기 때문에 일단 README.MD의 Example을 미리 업로드합니다.(아시는 분들은 유형 보호의 설명 부분을 건너뛰셔도 됩니다.)
// define the union type
const Lang = {
  ja: "ja",
  en: "en",
} as const;
type Lang = typeof Lang[keyof typeof Lang]; // "ja" | "en"

const langList = Object.values(Lang);

type Post = {
  id: number;
  author: string | null;
  body: string;
  lang: Lang;
  isPublic: boolean;
  createdAt: Date;
  tags?: string[] | null;
};

// create a scanner
const isPost = scanner<Post>({
  id: number,
  author: union(string, Null),
  body: string,
  lang: list(langList),
  isPublic: boolean,
  createdAt: date,
  tags: optional(array(string), Null),
});

const data = {
  id: 1,
  author: "taro",
  body: "Hello!",
  lang: "ja",
  isPublic: true,
  createdAt: new Date(),
  tags: ["tag1", "tag2"],
} as unknown;

// scan
const post = scan(data, isPost);

post.body; // OK

형 방어는 무엇입니까?


TypeScript를 사용하면 any형, unknown형, union형 등 실제 값 유형이 불분명한 변수를 처리하는 경우가 많다.여기서 fetch API 등을 통해 API에서 데이터를 가져오면 여러 속성이 있는 객체가 처리됩니다.fetch API의 경우 얻은 데이터 값은 any형이다.제닉스as가 반환값의 유형을 지정할 수도 있지만 일부 속성에 예상치 못한 값이 있거나 필요하지 않은 속성이 있어도 데이터를 얻을 때 오류가 발생하지 않는다.이 속성을 실제로 보는 시점에서 처음으로 오류가 발생했습니다.
대부분의 경우 어느 곳에서 데이터 값을 사용하기 때문에 개발할 때 오류를 발견할 수 있다.그러나 실제 이 값을 참조하기 전에는 데이터의 불완전함과 유형 정의의 오류 등에 주의하지 않는다.이 문제는 데이터를 얻은 후 바로 모델을 검증하고 문제가 있으면 잘못된 방법으로 해결할 수 있다.그걸 실현하기 위해 필요한 건 유형 방어(Type Guard)야.
fetch API에서 데이터를 얻는 경우 외부 서비스의 API 등 응답이 갑자기 바뀌지 않기 때문에 진행형 보호의 장점이 전혀 없다고 본다.제 경우는 POST 요청에 포함된 바디를 만들 때 등 Object의 복잡한 처리를 직접 조작해 검증할 때 사용하는 경우가 많습니다.
아래의 예에서 string형인지 number형인지 판정한다.
const foo = "a" as unknown;

if (typeof foo === "string") {
  foo.toUpperCase(); // ok: string型として扱われる
}

if (typeof foo === "number") {
  foo * 1; // ok: number型として扱われる
}
형 보호는 유형 결단과 달리 실제 값의 유형을 판정한다.즉'typeof value === "string"true라면valuestring인 것이다.
또한 유형 보호 함수(Type Gurd 함수)를 다음과 같이 정의할 수도 있습니다.
// string型がどうかをチェックする関数
const isString = (value: unknown): value is string => typeof value === "string";

if (isString(foo)) {
  foo.toUpperCase(); // ok: string型として扱われる
  foo * 1; // error!
}
함수의 반환값 유형은 value is Type로 임의의 조건문을 반환하여 정의할 수 있다.
상기 isString()의 예에서 보듯이 사용형 보호 함수는 쓰기typeof value === "string"보다 쓰기가 유창하기 때문에 편리하지만 내용의 조건을 충족시키면string형으로 사용할 수 있다는 점에 주의해야 한다.
유형 보호 함수를 정의하여 여러 속성을 가진 Object의 유형 보호를 수행할 수 있습니다.
type Foo = {
  a: string;
  b: number;
}

const foo = {
  a: "a",
  b: 1,
} as unknown;

// 全てのプロパティを"Optional"とし、全ての値についてunknown型とした型を返す
type WouldBe<T> = { [P in keyof T]?: unknown };

// Objectかどうかを判定する型ガード関数
const isObject = <T extends Record<string, unknown>>(value: unknown): value is WouldBe<T> =>
  typeof value === "object" && value !== null;

// Foo型かどうかを判定する型ガード関数
const isFoo = (value: unknown): value is Foo =>
  isObject<Foo>(value) && typeof value.a === "string" && typeof value.b === "number";

if (isFoo(foo)) {
  foo.a.toUpperCase(); // ok
  foo.b * 1; // ok
}
위의 예는 @suin씨가 이 글에서 소개한 코드와 기본적으로 같기 때문에 상세한 해설을 생략합니다.멋진 보도 감사합니다!
https://qiita.com/suin/items/e0f7b7add75092196cd8

유형 보호 함수의 문제점


유형 보호 함수를 사용하면 여러 속성을 가진 복잡한 Object의 유형 보호를 수행할 수 있습니다.나도 최근에 자주 사용했다as. 그러나 형 보호 함수의 존재를 알고 필요에 따라 형 보호를 잘 했다.그러나 Object형 보호 구현에 문제가 발생했습니다.

가독성이 낮다

isFoo의 예를 보면 코드의 전망이 좋지 않다는 것을 알 수 있다.Foo 속성은 유형의 대상보다 많고, typeof 연산자나 instanceof 연산자가 검사할 수 없는 유형"a" | "b", string[] 등이 있으면 더욱 복잡해진다.
// union型を定義
const Lang = {
  ja: "ja",
  en: "en",
} as const;
type Lang = typeof Lang[keyof typeof Lang]; // "ja" | "en"

const langList = Object.values(Lang); // ["ja", "en"]

// Post型を定義
type Post = {
  id: number;
  author: string | null;
  body: string;
  lang: Lang;
  isPublic: boolean;
  createdAt: Date;
  tags?: string[] | null;
};


// Postの型ガード関数
const isPost = (value: unknown): value is Post =>
  isObject<Post>(value) &&
  typeof value.id === "number" &&
  (typeof value.author === "string" || value.author === null) &&
  typeof value.body === "string" &&
  langList.includes(value.lang as any) &&
  typeof value.isPublic === "boolean" &&
  value.createdAt instanceof Date &&
  (
    value.tags == null ||
    (
      Array.isArray(value.tags) &&
      value.tags.every((item) => typeof item === "string")
    )
  );

// Post型の条件を満たすデータ
const data = {
  id: 1,
  author: "taro",
  body: "Hello!",
  lang: "ja",
  isPublic: true,
  createdAt: new Date(),
  tags: ["tag1", "tag2"],
} as unknown;

if (isPost(data)) {
  console.log(data.body.trim()); // ok
}
이번에는 사용하지 않는 보호 함수를 시험적으로 실시하였다.보시다시피 이러다가는 가독성이 낮아 보기 힘들어요.다음은 isString()와 같은 독특한 유형의 보호 함수를 사용하여 isPost()를 팩스로 보냅니다.

형식 보호 함수를 정의하는 것은 매우 힘들다


팩스 코드 여기 있습니다.
// リファクタリングしたisPost()
const isPost = (value: unknown): value is Post =>
  isObject<Post>(value) &&
  isNumber(value.id) &&
  (isString(value.author) || isNull(value.author)) &&
  isString(string) &&
  isList(value.lang, langList) && // Lang型の型ガード関数 ("ja" | "en")
  isBoolean(value.isPublic) &&
  isDate(value.createdAt) &&
  (isNull(value.tags) || isUndefined(value.tags) || isArray(value.tags, isString));
이렇게 되면 가독성이 약간 높아진다.isList()만 설명하면 첫 번째 파라미터에 주어진 값이 두 번째 파라미터에 포함된 그룹의 유형 보호 함수인지 확인하는 데 사용됩니다.
형 보호 함수의 정의 부분을 생략했지만 이번 팩스에서 사용한 함수만으로는 정의하기 어렵다.isString()와 같은 원시 유형의 유형 보호 함수는 특별하지만 기본적인 유형 보호 함수는 매번 같은 실현이기 때문에 프로그램 라이브러리로 함께 준비하면 편리하다.

어떤 속성에 문제가 있는지 알기 어렵다


함수의 가독성 문제는 사라졌지만 실제 사용형 보호 함수에 문제가 생겼다.
const post = await fetchPost();
if (!isPost(post)) throw new Error("post is invalid.")

// postでなにかする
유형 검증에 실패했을 때 던지는 오류 처리입니다.
형 방어 자체는 좋지만 이 오류 메시지에서'어떤 속성이 이상한가'를 바로 판단할 수는 없다.post확인console.log내용 등으로 실제 값과 형 보호 함수의 내용을 비교하여 오류를 찾아야 한다.Object의 속성이 많을수록 쉽지 않습니다.
이 문제를 해결하기 위해서는 봉인형 보호 함수를 설치하고 오류가 발생할 때 '문제 속성' 정보를 포함하는 오류 문장의 함수를 방출해야 한다.

typescanner 소개


오프닝이 길어졌습니다. 상기 문제를 해결하기 위해 제작된 프로그램 라이브러리는 typescanner입니다.다음은 순서대로 설명하겠습니다.
지금의 규격과 다른 점이 있다.자세한 내용은 README를 보십시오.

typescanner의 특징

  • isString()를 포함하는 기본 유형 보호 함수 + 독특한 유형 보호 함수
  • Object의 유형 보호 함수scanner() 함수
  • 를 직관적으로 정의할 수 있음
  • 유형 보호를 바탕으로 검증된 값을 수신scan() 함수
  • 1. 기본 보호 함수


    typescanner가 준비한 형식 보호 함수는 다음과 같습니다.isArray 이후의 유형 보호 함수는 첫 번째 파라미터의 값, 두 번째 파라미터의 이후 검증에 필요한 (유형 보호 함수, 배열, 구조기 등)을 받아들이는 형식이다.
    // primitive
    
    isString("a") // true
    
    isNumber(1) // true
    
    isBoolean(true) // true
    
    isUndefined(undefined) // true
    
    isNull(null) // true
    
    isDate(new Data()) // true
    
    isSymbol(Symbol("a")) // true
    
    isBigint(BigInt(1)) // true
    
    // isObject
    
    isObject<T>(value) // <T>(value: unknown) =>  value is WouldBe<T>
    
    // isArray
    
    isArray(["a", "b"], isString) // string[]
    
    isArray<string | number>(["a", 1], isString, isNumber) // (string | number)[]
    
    isArray(["a", null, undefined], isString, isNull, isUndefined) // (string | null | undefined)[]
    
    // isOptional
    
    isOptional("a", isString) // true
    
    isOptional(undefined, isString) // true
    
    // isList
    
    isList("ja", langList) // true
    
    // isInstanceOf
    
    try {
      ...
    } catch (error) {
      if (isInstanceOf(error, Error)) {
        error.message // OK
      }
    }
    

    2.scanner() 함수

    scanner() 함수는 Object의 유형 보호 함수를 시각적으로 정의할 수 있는 함수입니다.
    "직관적으로 정의할 수 있음"← 주관적

    기본적

    scanner() 함수를 사용하여 아까 예에 등장한 Post형 보호 함수isPost()를 다시 쓴다.
    type Post = {
      id: number;
      author: string | null;
      body: string;
      lang: Lang;
      isPublic: boolean;
      createdAt: Date;
      tags?: string[] | null;
    };
    
    // scanner()関数で定義したPost型の型ガード関数
    const isPost = scanner<Post>({
      id: number,
      author: union(string, Null),
      body: string,
      lang: list(langList),
      isPublic: boolean,
      createdAt: date,
      tags: optional(array(string), Null),
    });
    
    scanner() 함수는Generix가 지정한 유형에 따라 각 속성에 대해 유형 보호 함수를 설정하여 Object의 유형 보호 함수를 되돌려줍니다.즉 scanner()의 매개 변수로 교부된 Object에 등장string,number,list(),optional() 등이 유형 보호 함수(또는 반환형 보호 함수의 함수)라는 것이다.
    이후stringoptional() 등에서 scanner() 함수를 사용할 때 사용하는 유형 보호 함수를fields라고 한다.string와 같은 원시형 필드의 내용은 isString()와 똑같다. 타입Ailias로 정의된 느낌에 가까워지고 싶어서 그렇게 했다('직관적으로 정의할 수 있다'는 것은 여기서 나온 것이다).
    물론 마음에 들지 않으면 string로도 대체할 수 있다.

    fields 확장


    또한 필드를 확장하기 위해 자신의 형식 보호 함수를 사용할 수 있습니다.
    type Foo = {
      a: string;
      b: number;
      c: boolean;
      d: Date;
      e: string[];
      f?: string;
      g: "a" | "b" | "c";
      h: string | null;
      i: string | number;
      j: number;
    };
      
    // 独自の型ガード関数 (field)
    const even = (value: unknown): value is number =>
      isNumber(value) && value % 2 === 0;
    
    const isFoo = scanner<Foo>({
      a: string,
      b: number,
      c: boolean,
      d: date,
      e: array(string),
      f: optional(string),
      g: list(["a", "b", "c"]),
      h: union(string, Null),
      i: union<string | number>(string, number),
      j: even, // Custom field
    });
    

    오류 발생 시 동작


    또한 유형 보호 함수의 문제점에 열거된'어떤 속성에 문제가 있는지 알기 어렵다'는 문제는'문제가 있는 속성'이라는 정보를 포함하는 오류문을 토해내어 해결한다.
    소박한 기능이지만 너무 편해서 마음에 들어요.
    // Error: value.key does not meet the condition.
    if (isFoo(data)) {
      ...
    }
    

    3.scan() 함수

    isString 함수는 첫 번째 파라미터에서 검증된 값을 통해 두 번째 파라미터 다음에 유형 보호 함수를 전달하여 검증된 값을 얻을 수 있다.
    // success
    const data = scan(foo as unknown, isFoo);
    data.a // OK
    
    // Error!
    const data = scan(bar as unknown, isFoo); // Error: value.key does not meet the condition.
    
    함수는 최근 Zenn에서 화제가 된 @yuitosato의as-safely를 참고했다.
    https://zenn.dev/yuitosato/articles/fdbc464f31c292

    다른 라이브러리 비교


    typescanner의 scan() 함수와 비슷한 라이브러리도 있지만 함수 이름이 마음에 들지 않고 전체적인 전망이 좋지 않아 라이브러리 측에서 scan()scanner() 함수 같은 기능을 준비하기를 원한다. "그렇습니다!"이런 걸 못 찾았어.그리고 예전에 npm 포장을 만드는 소원이 있었기 때문에 이번에는 제가 직접 만들어 봤어요.

    최후


    마음에 드는 사람은 반드시 설치해서 사용하세요!풀 리퀘스트 등도 환영했다.잘못이나 이상한 점이 있다면 댓글로 지적해 주세요.

    참고 자료


    https://www.npmjs.com/package/typescanner
    https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard

    좋은 웹페이지 즐겨찾기