4 Typescript 제네릭 함수의 힘을 활용하는 방법에 대한 아이디어

Typescript는 자바스크립트 코드의 안정성을 크게 향상시키는 강력한 도구입니다. 그러나 개발자가 Typescript로 작업하는 동안 처리해야 하는 특정 오버헤드도 추가합니다.

제네릭 함수는 아마도 가장 까다롭지만 가장 강력한 Typescript 개념 중 하나일 것입니다. 제네릭이라는 주제를 간략하게 다루었지만 이제 더 깊이 파고들어 제네릭의 힘을 활용하여 확장 가능하고 재사용 가능한 코드를 제공할 수 있는 방법에 대해 이야기하고 싶습니다. 오늘 우리는 ❤️로 만들고 Typescript로 구동되는 일반 도우미 함수에 대한 네 가지 아이디어를 고려할 것입니다.

부인 성명



다양한 방법으로 궁극적인 솔루션을 찾고 있다면 ramda 또는 lodash 과 같은 기존 라이브러리를 확인하는 데 관심이 있을 수 있습니다. 이 게시물의 목적은 일상적인 개발에 유용하고 Typescript 제네릭의 그림에 적합한 몇 가지 예를 논의하는 것입니다. 의견에 자유롭게 사용 사례를 추가하고 함께 토론합시다 💪

내용의 테이블


  • Map by key
  • Group by key
  • Merge
  • Sort

  • 시작하기 전에



    예를 들어 두 개의 간단한 인터페이스를 생각해 내고 그 인터페이스에서 배열을 만들었습니다.

    interface Book {
      id: number;
      author: string;
    }
    
    interface Recipe {
      id: number;
      cookingTime: number;
      ingredients: string[];
    }
    
    const books: Book[] = [
      { id: 1, author: "A" },
      { id: 2, author: "A" },
      { id: 3, author: "C" }
    ]
    
    const recipes: Recipe[] = [
      { id: 1, cookingTime: 10, ingredients: ["salad"] },
      { id: 2, cookingTime: 30, ingredients: ["meat"] }
    ]
    


    1. 키로 매핑

    interface Item<T = any> {
      [key: string]: T
    }
    
    function mapByKey<T extends Item>(array: T[], key: keyof T): Item<T> {
      return array.reduce((map, item) => ({...map, [item[key]]: item}), {})
    }
    

    Let's look closer to what happens here:

    1. interface Item<T = any> { ... } is a generic interface, with a default value of any (yes you can have default values in generics 🚀)
    2. <T extends Item>(array: T[], key: keyof T) : Type T is inferred from the parameter, but it must satisfy the condition <T extends Item> (in other words T must be an object).
    3. key: keyof T second parameter is constrained to the keys which are only available in T . If we are using Book , then available keys are id | author .
    4. (...): Item<T> is a definition of the return type: key-value pairs, where values are of type T

    Let's try it in action:

    mapByKey(books, "wrongKey") // error. Not keyof T -> (not key of Book)
    
    mapByKey(books, "id") // {"1":{"id":1,"author":"A"},"2":{"id":2,"author":"A"},"3":{"id":3,"author":"C"}}
    

    As you can see, we can now benefit from knowing in advance available keys. They are automatically inferred from the type of the first argument. Warning: this helper is handy with unique values like ids; however, if you have non-unique values, you might end up overwriting a value which was previously stored for that key.

    2. 키로 그룹화

    This method is beneficial, if you need to aggregate data based on a particular key, for instance, by author name.

    We start by creating a new interface, which will define our expected output.

    interface ItemGroup<T> {
      [key: string]: T[];
    }
    
    function groupByKey<T extends Item>(array: T[], key: keyof T): ItemGroup<T> {
      return array.reduce<ItemGroup<T>>((map, item) => {
        const itemKey = item[key]
        if(map[itemKey]) {
          map[itemKey].push(item);
        } else {
          map[itemKey] = [item]
        }
    
        return map
      }, {})
    }
    

    It's interesting to note, that Array.prototype.reduce is a generic function on its own, so you can specify the expected return type of the reduce to have better typing support.

    In this example, we are using the same trick with keyof T which under the hood resolves into the union type of available keys.

    groupByKey(books, "randomString") // error. Not keyof T -> (not key of Book)
    groupByKey(books, "author") // {"A":[{"id":1,"author":"A"},{"id":2,"author":"A"}],"C":[{"id":3,"author":"C"}]}
    

    3. 병합

    function merge<T extends Item, K extends Item>(a: T, b: K): T & K {
      return {...a, ...b};
    }
    

    In the merge example T & K is an intersection type. That means that the returned type will have keys from both T and K .

    const result = merge(books[0], recipes[0]) // {"id":1,"author":"A","cookingTime":10,"ingredients":["bread"]}
    result.author // "A"
    result.randomKey // error
    

    4. 정렬

    What is the problem with Array.prototype.sort method? → It mutates the initial array. Therefore I decided to suggest a more flexible implementation of the sorting function, which would return a new array.

    type ValueGetter<T = any> = (item: T) => string | number;
    type SortingOrder = "ascending" | "descending";
    
    function sortBy<T extends Item>(array: T[], key: ValueGetter<T>, order: SortingOrder = "ascending") {
      if(order === "ascending") {
        return [...array].sort((a, b) => key(a) > key(b) ? 1 : -1 )
      }
      return [...array].sort((a, b) => key(a) > key(b) ? -1 : 1 )
    }
    

    We will use a ValueGetter generic function, which will return a primitive type: string or number. It is a very flexible solution because it allows us to deal with nested objects efficiently.

    // Sort by author
    sortBy(books, (item) => item.author, "descending")
    
    // Sort by number of ingredients
    sortBy(recipes, (item) => item.ingredients.length)
    
    // Sort very nested objects
    const arrayOfNestedObjects = [{ level1: { level2: { name: 'A' } } }]
    sortBy(arrayOfNestedObjects, (item) => item.level1.level2.name)
    

    요약



    이 포스트에서 우리는 JS 배열과 객체에 대한 일반적인 작업을 위한 도우미 함수를 작성하여 Typescript의 일반 함수를 가지고 놀았습니다. Typescript는 재사용 가능하고 구성 가능하며 유형이 안전한 코드를 생성할 수 있는 다양한 도구를 제공합니다.

    제 게시물이 마음에 드셨다면 널리 퍼트려주시고 🚀웹 개발에 관한 더 흥미로운 콘텐츠를 만나보세요.

    좋은 웹페이지 즐겨찾기