ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [이펙티브 타입스크립트] 2장. 타입스크립트의 타입 시스템 (2)
    웹/TypeScript 2023. 6. 1. 09:48

    12. 함수 표현식에 타입 적용하기

     

    타입 스크립트에서는 함수 표현식을 사용하는 것이 좋다.

    반복되는 함수 시그니처를 하나의 함수 타입으로 통합할 수 있다. 

     

    ex) 

    type ArithmeticFunction = (num1: number, num2: number) => number;
    
    const add: ArithmeticFunction = (num1, num2) => {
      return num1 + num2;
    };
    
    const subtract: ArithmeticFunction = (num1, num2) => {
      return num1 - num2;
    };
    
    const multiply: ArithmeticFunction = (num1, num2) => {
      return num1 * num2;
    };
    
    const divide: ArithmeticFunction = (num1, num2) => {
      return num1 / num2;
    };

     

     

     

    13. 타입과 인터페이스의 차이점 알기

     

    Type: 

    type은 기존 타입을 별칭으로 정의하는 데 사용됩니다. 즉, 기존의 타입을 사용자 정의 타입으로 나타내기 위해 사용됩니다. 타입 별칭은 원시 타입, 유니온 타입, 튜플, 객체 타입 등 다양한 타입을 정의할 수 있습니다. 타입 별칭은 Union, Intersection, Type Assertion과 같은 고급 타입 기능을 지원합니다. 타입 별칭은 = 기호를 사용하여 정의됩니다. 

     

    Interface: 

    interface는 객체의 구조를 정의하기 위해 사용됩니다. 즉, 객체의 속성, 메서드, 인덱서 등을 설명하는 데 사용됩니다. 인터페이스는 객체 타입에 대한 명세를 제공하고, 클래스가 해당 인터페이스를 구현하거나 객체가 해당 인터페이스를 준수하는지 검사하는 데 사용됩니다. 인터페이스는 상속과 확장이 가능하며, 다중 상속이 지원됩니다. 인터페이스는 interface 키워드를 사용하여 정의됩니다. 

     

    주요 차이점: 

    타입 별칭(Type)은 일반적으로 유니온 타입, 인터섹션 타입, 기본 타입 등을 정의하는 데 사용되며, 객체의 구조를 명시적으로 정의할 수 없습니다. 반면 인터페이스(Interface)는 객체의 구조를 명시적으로 정의하고 클래스와 객체에 대한 명세를 제공하는 데 사용됩니다. 

     

    타입 별칭(Type)은 타입으로 사용될 수 있는 다양한 요소들을 정의할 수 있습니다. 인터페이스(Interface)는 주로 객체의 구조와 동작을 설명하는 데 사용됩니다. 

     

    타입 별칭(Type)은 Union, Intersection, Type Assertion 등과 같은 고급 타입 기능을 지원합니다. 반면 인터페이스(Interface)는 확장과 상속을 지원합니다. 타입 별칭(Type)은 type 키워드를 사용하여 정의되고, 인터페이스(Interface)는 interface 키워드를 사용하여 정의됩니다. 

     

    유의할 점: 

    타입 별칭(Type)은 일반적으로 유니온 타입, 인터섹션 타입 등과 같이 복잡한 타입을 정의할 때 유용합니다. 인터페이스(Interface)는 객체의 구조와 동작을 명시적으로 정의하고 클래스와 객체에 대한 명세를 제공하는 데 주로 사용됩니다. 

     

    프로젝트 내에서 일관성을 유지하기 위해 타입 별칭(Type)과 인터페이스(Interface)를 적절하게 사용해야 합니다. 예를 들어, 객체의 구조를 명시적으로 설명해야 하는 경우에는 인터페이스를 사용하는 것이 좋습니다. 

     

    타입 별칭(Type)과 인터페이스(Interface)는 함께 사용할 수 있습니다. 필요에 따라 두 가지를 혼합하여 타입을 정의하고 사용할 수 있습니다.

     

    참고로 타입과 인터페이스 모두 제너릭이 가능하다.

     

    타입과 인터페이스의 차이를 알고, 같은 상황에서는
    동일한 방법으로 명명된 타입을 정의해 일관성을 유지해야 한다. 


     

    14. 타입 연산과 제너릭 사용으로 반복 줄이기 

     

    ※ DRY 원칙 

     

     

    타입 중복은 코드 중복만큼 많은 문제를 발생시킬 수 있다. 

    따라서, 타입에 이름을 붙여 타입 반복 사용을 줄여야 한다.

     

     

    ※ 제너릭 타입

    제네릭(Generic)은 TypeScript에서 재사용 가능한 코드를 작성할 때 사용되는 강력한 기능입니다. 제네릭을 사용하면 함수나 클래스에서 사용되는 타입을 동적으로 지정할 수 있으므로, 코드의 재사용성을 높이고 중복을 줄일 수 있습니다.

     

     

    15. 동적 데이터에 인덱스 시그니처 사용하기

     

    JS 객체는 문자열 키를 타입의 값과 관계없이 매핑한다. 

    타입에 인덱스 시그니처를 명시하여 유연하게 매핑을 표현할 수 있다. 

     

    인덱스 시그니처란 ? 

    type IndexType = {
      [key: 타입]: 값의 타입;
    }

    유의점 ) 

    1. 잘못된 키를 포함해서 모든 키를 허용한다.
    2. 특정 키가 필요하지 않다.
    3. 키마다 다른 타입을 가질 수 없다.
    4. 키 자동완성 기능을 사용할 수 없다.

     

    따라서, 가능한 정확한 타입을 지정하는 것이 좋고,
    런타임때까지 객체의 속성을 알 수 없을 때 (csv 파일에서 로드 하는 경우 등 ) 인덱스 시그니처를 사용하자. 

     

     

    16. number 인덱스 시그니쳐보다는 Array, 튜플, ArrayLike를 사용하기

     

    요약 ) 


    1. 자바스크립트에서는 object의 key 타입은 string 타입 혹은 symbol 타입이다. 다른 타입은 형변환된다.
    2. 타입스크립트에서는 자바스크립트와의 일관성을 위해 number 타입의 key 타입을 허용한다.
    3. number 인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하자.


    배열은 객체이므로 키는 숫자가 아니라 문자열이다. 

     

     

    17. 변경 관련된 오류 방지를 위해 readonly 사용하기

     

    오류의 범위를 좁히기 위해 함수 매개변수의 readonly 접근 제어자를 사용한다. 

     

    `readonly` 접근 제어자는 TypeScript에서 사용되는 속성 및 매개변수에 적용할 수 있는 한정자입니다. 이를 통해 해당 속성 또는 매개변수가 읽기 전용(변경할 수 없는)임을 나타낼 수 있습니다.

    `readonly` 접근 제어자를 사용하면 다음과 같은 효과를 얻을 수 있습니다:

    할 수 있는 것:
    1. 속성 값을 초기화한 후에는 값을 변경할 수 없습니다.
    2. 속성 값을 읽을 수 있습니다.
    3. 읽기 전용 속성에 대한 접근자(getter)를 정의할 수 있습니다.

    할 수 없는 것:
    1. 읽기 전용 속성에 값을 할당하거나 변경할 수 없습니다.
    2. 읽기 전용 속성에 대한 설정자(setter)를 정의할 수 없습니다.

    다음은 `readonly` 접근 제어자를 사용한 예시 코드입니다:

    class Person {
      readonly name: string;
      readonly age: number;
    
      constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
      }
    
      getDetails(): string {
        return `Name: ${this.name}, Age: ${this.age}`;
      }
    }
    
    const person = new Person("John", 30);
    
    console.log(person.name); // "John"
    console.log(person.age); // 30
    console.log(person.getDetails()); // "Name: John, Age: 30"
    
    person.name = "Mike"; // 컴파일 에러: Cannot assign to 'name' because it is a read-only property.



    위의 예시에서 `name`과 `age` 속성은 `readonly` 접근 제어자로 선언되어 있습니다. 이로 인해 속성 값은 생성자에서 초기화된 후에는 변경할 수 없습니다. 그러나 속성 값은 읽을 수 있으며, `getDetails()` 메서드를 통해 읽기 전용 속성에 접근할 수 있습니다.

    `readonly` 접근 제어자를 사용하면 읽기 전용 속성을 통해 데이터의 불변성을 보장하고, 의도치 않은 변경을 방지할 수 있습니다.

     

    readonly는 얕게 동작한다는 것을 유의하자.

     

     `readonly`는 얕게(shallow) 동작합니다. 이는 `readonly`로 선언된 객체나 배열의 속성에 대해 해당 속성의 참조를 변경할 수 없지만, 속성이 참조하는 객체나 배열의 내용은 변경할 수 있다는 의미입니다.

    다음은 `readonly`가 얕게 동작하는 예시 코드입니다:

    interface Person {
      readonly name: string;
      readonly hobbies: string[];
    }
    
    const person: Person = {
      name: "John",
      hobbies: ["reading", "swimming"],
    };
    
    person.name = "Mike"; // 컴파일 에러: Cannot assign to 'name' because it is a read-only property.
    
    person.hobbies.push("cooking"); // 속성의 배열은 수정 가능
    console.log(person.hobbies); // ["reading", "swimming", "cooking"]
    
    person.hobbies[0] = "gardening"; // 속성의 배열의 요소는 수정 가능
    console.log(person.hobbies); // ["gardening", "swimming", "cooking"]



    위의 예시에서 `Person` 인터페이스의 `name` 속성은 `readonly`로 선언되어 있으므로 값을 변경할 수 없습니다. 하지만 `hobbies` 속성은 배열이므로 `push` 메서드를 사용하여 새로운 요소를 추가하거나 인덱스를 통해 요소를 변경할 수 있습니다.

    따라서 `readonly`는 속성의 참조를 변경하는 것을 방지하지만, 속성이 참조하는 객체나 배열의 내용을 변경하는 것은 허용합니다. 이를 유의하여 `readonly`를 사용할 때 해당 속성이 참조하는 객체의 변경 여부를 고려해야 합니다. 만약 속성이 참조하는 객체도 변경되지 않아야 한다면, 깊은 읽기 전용(Deep Readonly)을 구현하기 위해 재귀적으로 `readonly`를 적용하거나 별도의 불변성 라이브러리를 사용해야 합니다.

     



    `readonly`로 선언된 배열에 대해 단언문을 사용하여 배열의 수정을 방지할 수 있습니다. 단언문을 사용하여 타입 시스템에게 해당 배열이 수정되지 않아야 한다고 명시적으로 알릴 수 있습니다.

    다음은 `readonly` 배열에 대한 단언문을 사용하여 배열 수정을 방지하는 예시 코드입니다:

    const readonlyArray: readonly string[] = ["apple", "banana", "cherry"];
    
    readonlyArray.push("date"); // 컴파일 에러: Property 'push' does not exist on type 'readonly string[]'.
    readonlyArray[0] = "avocado"; // 컴파일 에러: Index signature in type 'readonly string[]' only permits reading.


    위의 예시에서 `readonlyArray`는 `readonly`로 선언된 문자열 배열입니다. 따라서 해당 배열은 읽기 전용이므로 `push` 메서드로 요소를 추가하거나 인덱스를 통해 요소를 변경하는 등의 수정 작업을 할 수 없습니다.

    `readonly`로 선언된 배열에 대해 수정 작업을 방지하기 위해 단언문을 사용하면 컴파일러가 해당 작업을 감지하여 에러를 발생시킵니다. 이를 통해 의도치 않은 수정을 방지할 수 있습니다.

    하지만 단언문은 컴파일 타임에만 유효하므로, 런타임에서는 배열의 수정을 방지하지 않습니다. 따라서 실제로 런타임에서 배열의 수정을 막아야 한다면 `readonly` 배열 대신 `ReadonlyArray` 타입을 사용하는 것이 더 안전합니다. `ReadonlyArray`는 배열이 실제로 읽기 전용이 되도록 보장합니다.

     

     

     

    18. 매핑된 타입을 사용하여 값을 동기화하기

     

    매핑된 타입은 한 객체가 또 다른 객체와 정확히 같은 속성을 가지게 할 때 이상적이다

     

    const REQUIRES_UPDATE: { [k in keyof ScatterProps]: boolean } = {
      xs: true,
      ys: true,
      xRange: true,
      yRange: true,
      color: true,
      onClick: false,
    };
    
    function shouldUpdate(oldProps: ScatterProps, newProps: ScatterProps) {
      let k: keyof ScatterProps;
      for (k in oldProps) {
        if (oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
          return true;
        }
      }
      return false;
    }

    typescript [k in keyof ScatterProps] 은 타입 체커에게 REQUIRES_UDPATE  ScatterProps 와 동일한 속성을 가져야한다는 정보를 제공합니다.

    나중에 ScatterProps 에 새로운 속성을 추가하는 경우 다음 코드와 같은 형태가 됩니다.

    interace ScatterProps {
      // ... Error onDoubleClick 속성이 타입에 없습니다.
      onDoubleClick: () => void;
    }

    이런 방식은 오류를 정확히 잡아 냅니다. 속성을 삭제하거나 이름을 바꾸어도 비슷한 오류가 발생합니다.

     매핑된 타입을 사용해서 관련된 값과 타입을 동기화하도록 합시다.
    인터페이스에 새로운 속성을 추가할 때, 선택을 강제하도록 매핑된 타입을 고려해야 합니다.

     
Designed by Tistory.