[4장] 타입 확장하기 · 좁히기 - 북 스터디

코딩 마을 방범대 북 스터디 DAY 3

·

9 min read

[4장] 타입 확장하기 · 좁히기 - 북 스터디
🤓 스터디원 🤓
강지윤 @eeeyooon
이예솔 @lulla-by
이성령 @sryung1225
이에스더 @Stilllee
채하은 @chaehaeun



코딩 마을 방범대 : 4장. 타입 확장하기 · 좁히기

질문과 답변

1. 타입가드

타입가드의 종류에는 무엇이 있으며 각각 어떤 상황에서 사용하나요?

타입가드는 변수 또는 표현식의 타입 범위를 좁혀나가 더 정확하고 명시적인 타입 추론을 가능하게 하며 복잡한 타입을 작은 타입으로 축소하여 타입의 안전성을 높일 수 있습니다.

이러한 타입가드는 대표적으로 자바스크립트 연산자를 사용한 타입가드와 사용자 정의 타입가드로 나뉘는데요.

자바스크립트 연산자를 이용한 3가지의 대표적인 타입가드 연산자와 사용자 정의 타입가드를 사용하는 연산자를 말해주세요.

그리고 각각의 연산자는 어떤 상황에서 사용이 되는지 간단하게 설명해주세요.

Stilllee
자바스크립트 연산자를 활용한 타입가드
런타임에 유효한 타입 가드를 만들기 위해 사용한다.
1. typeof
원시 타입을 추론할 때 사용한다.

let x: any = "Hello TypeScript!";

if (typeof x === "string") {
console.log(x); // "Hello TypeScript!"
} else {
console.log("x is not a string");
};

2. instanceof
인스턴스화된 객체 타입을 판별할 때 사용한다.

let x: any = "Hello TypeScript!";

if (typeof x === "string") {
console.log(x); // "Hello TypeScript!"
} else {
console.log("x is not a string");
};

3. in
객체의 속성이 있는지 없는지에 따른 구분을 할 때 사용한다.

let protector = { name: "철수", mbti: 'ESTJ'};

if ("mbti" in protector) {
console.log(방범대원 ${protector.name}의 MBTI는 ${protector.mbti}입니다.); 
// "방범대원 철수의 MBTI는 ESTJ입니다."
} else {
console.log("mbti does not exist in protector");
};

사용자 정의 타입 가드
사용자가 직접 어떤 타입으로 값을 좁힐지를 직접 지정하는 방식이다.

// is 연산자를 활용한 예시

function isNumber(value: any): value is number {
  return typeof value === 'number';
}

let myValue: any = 5;

if (isNumber(myValue)) {
  console.log('myValue is a number'); // 출력
} else {
  console.log('myValue is not a number');
};

myValue = 'five';

if (isNumber(myValue)) {
    console.log('myValue is a number');
} else {
    console.log('myValue is not a number'); // 출력
};
eeeyooon
타입 가드로 활용하는 자바스크립트 연산자로는 원시 타입을 추론할 때 주로 쓰이는 typeof 연산자, 인스턴스화된 객체 타입을 판별할 때 쓰이는 instanceof 연산자, 객체의 속성이 있는지 없는지에 따라 구분할 때 쓰이는 in 연산자가 있습니다.

사용자가 직접 어떤 타입으로 값을 좁힐지를 지정할 때 is 연산자로 타입 명제인 함수를 정의합니다.
ryung1225
자바스크립트 연산자를 이용한 3가지 대표적인 타입가드 연산자
  • typeof : 원시타입을 추론할 때 사용
  • instance of : typeof의 한계와 연관되며 인스턴스화된 객체 타입을 판별할 때 사용
  • in: 객체의 속성이 들어있는지 여부에 따라 구분할 때 사용

  • 사용자정의 타입가드를 이용하는 연산자
  • is: 직접 어떤 타입으로 값을 좁힐지 지정하는 데에 사용
  • 출제자 : lulla-by

    타입가드 사용시 자바스크립트 연산자를 사용한 타입가드는 typeof, instanceof, in 세가지가 있습니다.

    사용자 정의 타입가드는 is 연산자를 사용하며 반환값의 타입명제에 A is B의 형식으로 사용합니다.

    다음은 각 연산자별 예시코드입니다.

    1. typeof는 원시 타입을 추론할 때 사용됩니다. 아래는 예시입니다.
      function printType(arg: number | string | boolean): void {
      if (typeof arg === 'number') {
       console.log('The type is number');
      } else if (typeof arg === 'string') {
       console.log('The type is string');
      } else if (typeof arg === 'boolean') {
       console.log('The type is boolean');
      } else {
       // TypeScript는 위에서 모든 가능한 타입을 체크했으므로 이 부분은 실행되지 않을 것
       console.log('Unknown type');
      }
      }
      
    2. instanceof 인스턴스화된 객체를 판별할 때 사용합니다. A intanceof B의 형태로 사용하며 A타입에는 검사할 대상 변수, B에는 특정 객체가 들어가며 A의 프로토타입 체인상에 B가 존재하는지를 판별해주며 존재하면 true를 존재하지 않는다면 false를 반환합니다.
    class Animal {
      name: string;
    
      constructor(name: string) {
        this.name = name;
      }
    }
    
    class Dog extends Animal {
      bark(): void {
        console.log('Wal!');
      }
    }
    
    const myDog = new Dog('Millky');
    
    console.log(myDog instanceof Animal); // true
    console.log(myDog instanceof Dog);    // true
    
    1. in연산자는 객체의 속성이 있는지 없는지 구분할 때 사용합니다. A in B의 형태로 사용하며 A라는 속성이 B 객체에 존재하는지 검사합니다. in 연산자는 B내부에 A 속성이 있는지 검사하기 때문에 undefined를 할당해도 true로 나옵니다. in 연산자 사용시 주의가 필요합니다.
    interface Person {
      name: string;
      job?: string;
    }
    
    const jay: Person = {
      name: 'Jay',
    };
    
    // 속성이 있는지 확인
    console.log('name' in jay);  // true
    console.log('job' in jay);   // false
    
    // undefined를 할당한 경우에도 true
    jay.job = undefined;
    console.log('job' in jay);
    
    1. 사용자 정의 타입가드는 is 연산자를 사용하며 반환 타입이 타입 명제인 함수를 정의하여 사용 할 수 있습니다. 타입 명제는 A is B를 사용하며 A는 매개변수의 이름이고 B는 타입입니다. 이는 반환값이 참일때 A 매개변수의 타입을 B 타입으로 취급하게 되며 반환값이 참일경우 true를 참이 아닐 경우 false를 반환하게 됩니다.
    // 사용자 정의 타입 가드
    function isString(value: any): value is string {
      return typeof value === 'string';
    }
    
    // 예시 사용
    function printStringLength(value: any) {
      if (isString(value)) {
        // value는 여기서 string으로 타입이 좁혀짐
        console.log(value.length);
      } else {
        console.log('Not a string');
      }
    }
    
    // 테스트
    printStringLength('Hello');  // 5
    printStringLength(123);      // Not a string
    




    2. 교차타입

    교차타입 활용하기

    본문에서 교차 타입은 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것으로 이해할 수 있다고 말했습니다.

    주어진 type을 이용해 새로운 타입을 정의하고, 이를 만족하는 객체를 생성해보세요.

    type Book = {
      title: string;
      author: string;
    };
    
    type SaleItem = {
      price: number;
    };
    
    // 1. 교차 타입을 사용하여 'BookSaleItem' 타입을 정의하세요
    
    // 2. 'BookSaleItem' 타입의 객체 'firstItem'을 생성하세요.
    
    eeeyooon
    
    // 1. 교차 타입을 사용하여 'BookSaleItem' 타입을 정의하세요
    
    type BookSaleItem = Book & SaleItem;
    
    // 2. 'BookSaleItem' 타입의 객체 'firstItem'을 생성하세요.
    
    const firstItem: BookSaleItem = {
        title: "해리포터와 비밀의 방",
        author: "조앤 k. 롤링",
        price: 15000,
    }
    
    ryung1225
    
    // 1. 교차 타입을 사용하여 'BookSaleItem' 타입을 정의하세요
    type BookSaleItem = Book & SaleItem;
    
    // 2. 'BookSaleItem' 타입의 객체 'firstItem'을 생성하세요.
    const firstItem: BookSaleItem = {
      title: "어린왕자",
      author: "생텍쥐페리",
      price: 300000,
    };
    
    lulla-by
    & 연산자를 사용하여 type연산자의 교차 타입을 만들 수 있습니다.
    
    type BookSaleItem = Book & SaleItem;
    
    const firstItem:BookSaleItem ={
    title:"우아한 타입스크립트",
    author:"우아한형제들 웹프론트엔드개발그룹",
    price:32000
    }
    

    출제자 : Stilllee

    type Book = {
      title: string;
      author: string;
    };
    
    type SaleItem = {
      price: number;
    };
    
    // 1. 교차 타입을 사용하여 'BookSaleItem' 타입을 정의하세요
    type BookSaleItem = Book & SaleItem;
    
    // 2. 'BookSaleItem' 타입의 객체 'firstItem'을 생성하세요.
    const firstItem: BookSaleItem = {
      title: "우아한 타입스크립트 with 리액트",
      author: '우아한형제들',
      price: 28800,
    }
    

    여기서 BookSaleItem 타입은 BookSaleItem 타입을 교차하여 만든 새로운 타입입니다.

    이 타입은 Book 타입의 titleauthor 속성뿐만 아니라 SaleItem 타입의 price 속성도 가지고 있어야 합니다.

    firstItem 객체는 이 BookSaleItem 타입을 따르고 있으며, 모든 필요한 속성 (title, author, price)을 포함하고 있습니다.

    이 예제는 교차 타입을 사용하여 복합적인 타입을 만들고, 이 타입에 맞는 객체를 어떻게 생성하는지를 잘 보여줍니다.




    3. 식별할 수 있는 유니온

    식별할 수 있는 유니온을 사용하는 이유는 무엇일까요

    다음은 식별할 수 있는 유니온(Discriminated Unions)을 이용하여 타입을 좁힌 예제입니다.

    type IPhone = {
        name: "아이폰",
        version: number,
    }
    type AirPod = {
        name: "에어팟",
        version: number,
        noisecancel: boolean,
    }
    
    type AppleProductType = IPhone | AirPod;
    const products: AppleProductType[] = [
        { name: "아이폰", version: 15 },
        { name: "아이폰", version: 15, noisecancel: true }, // 🚨 error
    ];
    

    위 예제는 타입 좁히기를 무사히 성공하여 products 배열의 요소 중 두번째는 에러를 출력하도록 만들었습니다.
    그렇다면 (1) 왜 타입 에러를 갖게 하는 것이 좋은 코드인지에 대한 설명과 (사실 이 부분은 복습이여요)
    (2) 위 코드가 식별할 수 유니온을 사용하기 이전에 아무 타입 에러도 뱉지 않는 코드는 어땠을 것 같은지 역으로 추론해주세요.

    Stilllee
    타입 에러를 갖게함으로써 코드의 안정성을 보장하고 잘못된 타입을 예방하여 버그를 줄이는 데 도움이 된다. 이는 개발자가 코드의 의도를 명확하게 표현하고, 예상치 못한 오류를 방지할 수 있게 해준다.
    
    type IPhone = {
        version: number,
    }
    type AirPod = {
        version: number,
        noisecancel: boolean,
    }
    
    type AppleProductType = IPhone | AirPod;
    const products: AppleProductType[] = [
        { version: 15 },
        { version: 15, noisecancel: true }, // 에러발생 x
    ];
    
    eeeyooon
    (1) Ipone과 AirPod은 타입별로 각자만의 고유한 필드를 가진다. Ipone의 chargeType과 feature, AirPod의 noisecancel이 바로 그 고유한 필드이다. Ipone과 AirPod의 유니온 타입인 AppleProductType의 원소를 갖는 배열 products는 Ipone의 고유 필드와 AirPod의 고유 필드를 모두 가지는 객체에 대해서는 타입 에러를 뱉어야 한다. 하지만 식별할 수 있는 유니온을 사용하지 않으면 위와 같은 상황에서도 별도의 타입 에러를 뱉지 않는다. 이런 상황에서 에러가 발생하지 않는다면 무수한 에러 객체가 생겨도 개발 과정에서 발견하지 못할 위험성이 커진다.
    예제 코드처럼 식별할 수 있는 유니온을 추가하여 타입마다 구분할 수 있는 판별자를 달아주면, 정확하지 않은 에러 객체에 대해선 타입 에러가 발생하여 발견하고 처리할 수 있다. (2)
    
    function call() {
      console.log("여보세요?");
    }
    
    type IPhone = {
        version: number,
        chargeType: string,
        feature: () => void,
    }
    type AirPod = {
        version: number,
        noisecancel: boolean,
    }
    
    type AppleProductType = IPhone | AirPod;
    const products: AppleProductType[] = [
        { version: 15, chargeType: "C", feature: call },
        { version: 15, feature: call, chargeType: "C", noisecancel: true }, // 에러가 발생하지 않음.
    ];
    
    다른 타입의 고유 필드까지 가지고 있는 에러 객체가 발생해도, 타입 에러가 발생하지 않아 에러 객체가 그대로 존재하는 문제가 발생할 수 있다. 이러한 에러 객체의 존재는 예상치 못한 문제의 원인이 되는 등 위험성을 가지고 있다.
    lulla-by
    (1) 타입 에러를 발생시켠 처음에 기대한 객체의 타입에서 타입간 호환으로 인해 발생할 수 있는 문제를 확인할 수 있습니다.

    (2) IPhone과 AirPot의 필드를 모두 가지는 객체가 있을 경우 에러를 발생시켜야 할 것 같지만 자바스크립트는 덕타이핑 기반이기 때문에 서로 호환이 되어 에러가 발생하지 않습니다.
    
    type IPhone = {
        version: number,
        chargeType: string
    }
    type AirPod = {
        version: number,
        noisecancel: boolean,
    }
    
    type AppleProductType = IPhone | AirPod;
    const products: AppleProductType[] = [
        {version: 15, chargeType: "C"},
        {version: 3,noisecancel:true },
        {version: 15,chargeType: "C", noisecancel:true }, // 에러가 발생하지 않음
    ];
    

    출제자 : ryung1225

    자바스크립트의 경우 덕 타입 언어이기 때문에 타입 에러가 우선 보여지지 않는 문제가 있습니다. 이 경우 개발자는 원치않은 에러를 뒤늦게 발견할 수 있기 때문에 개발자에게 에러를 미리 보여주도록한 코드가 더 안정적인 코드라고 할 수 있습니다.

    추론할 수 있는 과거 코드의 경우의 수는 많고 다른 분들이 대답해준 것 역시 정답입니다.
    제가 준비한 답안은 식별할 수 있는 유니온의 판별자 선정 과정을 거치기 전으로 코드를 추론하는 것입니다.

    /* 판별자로 name을 선정 후 */
    type IPhone = {
        name: "아이폰",
        version: number,
    }
    type AirPod = {
        name: "에어팟",
        version: number,
        noisecancel: boolean,
    }
    
    /* 판별자로 name을 선정 전 */
    type IPhone = {
        name: string,
        version: number,
    }
    type AirPod = {
        name: string,
        version: number,
        noisecancel: boolean,
    }
    

    두 타입 IPhoneAirpod 을 구분할 수 있도록 하기 위해서는 타입마다 구분할 수 있는 판별자가 필요하고 문제에서는 이 판별자가 name 입니다. 이 판별자로 인해 서로 포함 관계를 벗어남으로서 좁혀진 타입의 에러를 미리 확인하고 사용할 수 있게 됩니다.