[TS] Type-Guard

Jane·2023년 8월 15일
1

TypeScript

목록 보기
14/17
post-thumbnail

🤔 타입가드란?

  • 데이터의 타입을 알 수 없거나, 될 수 있는 타입이 여러 개라고 가정할 수 있는 경우 조건문을 통해 데이터의 타입을 좁혀나가는 것
  • TS에서 특정 타입을 확인하고 해당 타입에 대한 추가적인 동작을 수행하기 위해 사용되는 메커니즘

Type Narrowing

  • 아직 타입을 잘 모르는 데이터에 대해 우선 제네럴한 타입을 부여한 뒤 타입 가드를 사용하여 그 범위를 좁혀나가는 과정

🧐 타입가드는 왜 필요할까?

function sampleFunc(args: number | boolean): void {
  console.log(args + 1);
}
  • 이 예시 함수는 에러가 발생한다.

    '+' 연산자를 'number | boolean' 및 'number' 형식에 적용할 수 없습니다.

  • sampleFunc 함수의 매개변수로는 number 타입이 올 수도 있고 string 타입이 올 수도 있다.
    • 이 때문에 함수의 결과를 예측할 수 없으므로 에러가 발생하는 것이다.
    • 매개변수로 여러 타입이 가능한 상황에서 안전하게 매개변수를 사용하고 함수 실행 결과를 예측하려면 어떻게 해야 할까?

  • 위와 같은 상황에서 타입 가드 사용 시 코드 블록 내에서 변수의 타입을 좁혀서 컴파일러가 타입을 추론하고 해당 타입에 따라 코드를 분기할 수 있게 된다.
  • 이를 통해 더욱 안정적인 코드를 작성하고, 런타임 에러를 방지할 수 있다.
  • 다시 말해, 타입 가드를 사용하면 데이터의 타입에 따라 대응하며 방어적인 코드를 짤 수 있게 해준다.

⚔️ 타입가드의 종류

  • 타입가드는 방법론적인 이야기이므로, 다양하게 구체화된 방법들이 존재한다.

🌟 typeof

function sampleFunc(args: number | boolean): void {
  // if문 내에서는 args가 number 타입임을 컴파일러가 확신할 수 있다.
  if (typeof args === "number") {
    console.log(args + 1);
  } else {
    // number이 아니면 boolean임을 추론한다.
    console.log(!args);
  }
}
  • JS에 이미 존재하는 타입 검사 연산자

  • 해당 조건문 코드 블록 내의 타입을 조건 분기를 통해 지정할 수 있다.

  • if문 뿐만 아니라 switch문을 사용한 타입 가드도 가능하다.

  • JS와의 호환을 위해 typeof로 가드할 수 있는 타입은 JS에서 제공하는 타입으로 제한된다.

    string, number, bigint, boolean, symbol, undefined, object, function
  • TS는 else를 이해하므로 if문으로 타입을 하나씩 좁혀갈 경우 else 문 안의 변수 타입은 절대 동일한 타입일 수 없음을 인지한다.

배열 타입 가드: Array.isArray()

function sampleFunc(args: number | number[]): void {
  // 배열인지 검사하기
  if (Array.isArray(args)) {
    console.log(args.slice(0, 1));
  } else {
    console.log(args + 1);
  }
}

🌟 instanceof

  • 생성자의 프로토타입 속성이 객체의 프로토타입 체인의 어디에 존재하는지 판별한다.
class Dog {
  bark = true;
  fly = false;
}

class Bird {
  fly = true;
  chirp = true;
}

function MyPet(args: Dog | Bird) {
  if (args instanceof Dog) {
    console.log(args.bark);
    console.log(args.fly);
  } else {
    // instanceof Bird로 추론됨
    console.log(args.bark); // ERROR!!
    console.log(args.fly);
  }
  console.log(args.fly);
  console.log(args.chirp); // ERROR!
}

MyPet(new Dog());
  • 타입 가드로 조건 분기 되지 않은 부분에서는 교집합에 해당하는 프로퍼티만 사용할 수 있다.

🌟 in

  • JS에 이미 존재하는 타입 검사 연산자
  • 객체가 특정 속성을 가지고 있는지 검사하고 boolean 타입 값으로 결과를 반환한다.
interface Cat {
  sleep: boolean;
  fly: boolean;
}

interface Bird2 {
  fly: boolean;
  chirp: boolean;
}

// sleep이라는 문자가 args 객체에서 key 속성으로 쓰였는지 검사
function NewPet(args: Cat | Bird2) {
  if ("sleep" in args) {
    console.log(args.fly); // fly, sleep 자동완성
  } else {
    console.log(args.fly); // never 형식에 fly 속성이 없습니다.
  }

  console.log(args.fly); // fly만 자동완성됨
}

const myCat: Cat = { sleep: true, fly: false };
NewPet(myCat);

🌟 리터럴 타입 가드

  • 리터럴 값의 경우 ===, ==, !==, != 연산자를 사용해 타입을 구분할 수 있다.

  • union 타입에 리터럴 타입이 있는 경우 공통 프로퍼티 값을 비교해 union 타입을 구분할 수 있다.

    • 예: key 값은 같고 value 값은 다른 속성명이 있는 경우

      type iphone = { type: "apple"; released: number };
      type galaxy = { type: "samsung"; liked: number };
      
      function myPhone(args: iphone | galaxy) {
        if (args.type === "apple") {
          console.log(args.released); // type, released 자동완성
        } else {
          console.log(args.liked); // type, liked 자동완성
        }
      }
    • 값이 다른 공통된 속성의 key에 접근해 구분해주는 방법도 존재한다.

    • 특정 속성이 있고 없고를 찾아 in으로 검사하기 번거로울 때 공통 속성명을 주어 객체의 라벨처럼 사용할 수 있다.

🌟 strictNullChecks; null과 undefined 타입 가드

  • 함수 로직을 수행하기 전 미리 데이터가 존재하는 값인지 검사할 수 있다.

    const fakeFunc = (props: unknown): void => {
      if (props === null || props === undefined) return;
      // 함수 로직...
    };
  • == null, != null 을 사용하면 null과 undefined는 모두 early return을 통해 걸러지고 a는 number로 추론된다.

    function foo(a?: number | null) {
      if (a == null) return;
      console.log(a + 1); // a는 number로 추론된다.
    }

🌟 사용자 정의 타입 가드

  • TS가 타입을 판단하는 방법을 직접 정의하거나 타입을 판단하는 로직을 재사용하고 싶을 때 사용한다.
  • 간단한 타입의 경우 위의 JS 제공 키워드를 통해 간단히 분기처리할 수 있지만
    복잡한 타입 가드의 경우 조금 더 전문적인 방법이 필요하다.
  • 이때 is 키워드를 사용하면 타입 가드 전용 함수를 만들 수 있다.
    • 함수의 return 값에 is 연산자를 명시해줌으로써 타입을 확정할 수 있는 헬퍼 함수의 역할을 한다.
interface yourDog {
  bark: number;
  fly: boolean;
}
interface yourBird {
  chirp: number;
  fly: boolean;
}

// 타입 가드 역할을 하는 커스텀 함수
// yourDog 타입인지 확인 하는 역할을 한다. (리턴 타입에 is 키워드를 붙여 사용한다.)
function dogOrBird(a: yourDog | yourBird): a is yourDog {
  // 직접 타입 판별 로직 구현 (bark라는 프로퍼티가 있다면)
  if ((a as yourDog).bark) {
    // bark, fly 자동완성
    return false; // 만일 개면 false
  } else {
    return true; // 만일 새면 true
  }
}

function pet(a: yourDog | yourBird) {
  if (dogOrBird(a)) {
    // 개일 경우
    console.log(a.bark); // bark, fly 자동완성
  } else {
    // 새일 경우
    console.log(a.chirp); // chirp, fly 자동완성
  }
}
  • 위의 함수에서 a is yourDog 부분을 return 값의 타입인 boolean으로 변경하면 에러가 발생한다.
    • a is yourDog는 사용자 정의 타입 가드를 지정하는 방식이다.
    • 컴파일러는 반환값인 true/false를 사용하여 a의 타입이 yourDog인지 yourBird인지 결정하여 해당 타입의 프로퍼티에 접근하게 된다.
    • 따라서 a is yourDog 부분을 단순 boolean으로 바꿀 경우 a의 타입이 둘 중 어떤 것인지 파악할 수 없게 된다.
    • 이로 인해 dogOrBird는 값을 하나로 좁히지 못하고, a에는 yourDog와 yourBird의 교집합인 fly 프로퍼티만 들어갈 수 있게 되므로 a.barka.chirp를 사용할 경우 에러가 발생하는 것이다.

🌟 콜백함수 타입 가드

// 콜백함수 타입가드

declare const FOO: { KEY?: { VALUE: string } };

function IIFE(callbackFunc: () => void) {
  callbackFunc();
}

// Type Guard
if (FOO.KEY) {
  console.log(FOO.KEY.VALUE);
  IIFE(() => {
    console.log(FOO.KEY.VALUE); // ERROR! 'FOO.KEY'은(는) 'undefined'일 수 있습니다.
  });
}
  • 위의 코드에서, optional 값인 FOO의 KEY가 존재할 때에만 아래의 코드가 실행되도록 타입 가드를 해주었음에도 에러가 발생하는 것을 확인할 수 있다.

🤔 왜 발생하는 에러일까?

  • 위의 조건문은 FOO.KEY라는 프로퍼티가 존재한다는 사실만 검증한다.
  • 콜백 함수인 IIFE의 실행 전까지는 FOO.KEY의 값이 변할 가능성이 존재하지 않는다.
  • 하지만 콜백 함수가 실행되면 호출 시점에 따라 다른 코드가 실행될 가능성, 그리고 그에 따라 객체의 값이 변경될 위험성이 존재한다.
  • 이러한 이유로 TS는 콜백함수 내에서 타입 가드가 계속 유효할 것이라고 기대하지 않는다.

🧐 어떻게 해결할 수 있을까?

  • 로컬 변수를 선언하고 그 안에 값을 할당하여 타입 추론이 가능하도록 만들 수 있다.
if (FOO.KEY) {
  console.log(FOO.KEY.VALUE);
  const val = FOO.KEY;
  IIFE(() => {
    console.log(val.VALUE);
  });
}
  • val에 FOO.KEY의 값을 할당해줌으로써 IIFE 내부에서 해당 값을 바로 참조할 수 있게 된다.
  • 이를 통해 해당 변수의 타입이 외부 요인으로 인해 바뀔 가능성이 없음을 보장한다.

🔎 References

profile
An investment in knowledge pays the best interest🙃

1개의 댓글

comment-user-thumbnail
2024년 12월 10일

그 제가 이제 막 배우기시작해서 그런지, 너무 헷갈리고 궁금한 부분이있는데

function dogOrBird(a: yourDog | yourBird): a is yourDog {
// 'bark' 프로퍼티가 있는지 확인
if ((a as yourDog).bark) {
return false; // 'bark'가 있으면 'yourDog'이므로 false 반환
} else {
return true; // 'bark'가 없으면 'yourBird'이므로 true 반환
}

이 부분 제가 이해가 잘 안가는데

코드 대로면
false일때 dog이고
true일떄 bird라는건데

function pet(a: yourDog | yourBird) {
if (dogOrBird(a)) {
// 개일 경우
console.log(a.bark); // bark, fly 자동완성
} else {
// 새일 경우
console.log(a.chirp); // chirp, fly 자동완성
}
}

그러면 이 부분이 반대로 되어야 하는거 아닌가영??

답글 달기

관련 채용 정보