Type Guard

조민호·2023년 1월 26일
0

union type의 문제점은 바로 공통된 프로퍼티만 사용을 할 수 있다는 것이었습니다


interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

// Developer | Person 타입을 리턴
function introduceSelf(): Developer | Person {
  return {
    name: "minho",
    age: 28,
  };
}

// Developer | Person 이므로 공통 특성인 name만 사용가능
const minho = introduceSelf();
console.log(minho.age); // error

이러한 문제점을 막기 위해 사용하는 것이 바로 type guard입니다

type guard를 통해 컴파일러가 타입을 예측할 수 있도록 타입을 좁혀 주어서(narrowing) 좀 더 타입의 안전성을 보장할 수 있습니다.



첫번째 방법. built-in Type Guard(typeof, instanceof)


JS에 이미 존재하는 typeof, instanceof 등의 연산자를 활용해서

type guard를 할 수 있습니다


typeof

typeof 연산자는 피연산자의 타입을 판단하여 문자열로 반환해 줍니다.

이 특성을 활용해서 아래의 예제는

if 문 내에서 arg는 무조건 string type임을 보장하게 합니다.

const testFunc = (arg: string | number) => {
  // arg.substring(3); // error

  if (typeof arg === 'string') {
    // 여기 밑에서 사용하는 arg는 string type으로 인식합니다.
    arg.substring(3);
  }
};

그렇지만 typeof의 치명적인 단점 2가지가 있습니다

  1. typeof 피연산자로 올 수 있는 것은 primitive 타입 및 객체로 제한이 됩니다.

    즉, 타입스크립트는 사용자가 직접 지정한 타입 혹은 인터페이스를 사용해 타입을 지정할 수 있는 장점도 매우 큰데,
    이러한 것들을 typeof로 검사를 할 수 없는 것입니다.

    class Developer {
      skill: string = 'a';
    }
    class Person {
      age: number = 32;
    }
    function introduceSelf(arg: Developer | Person) {
      if (typeof arg === 'Developer') { // error
    			// 직접 지정한 타입은 typeof로 검사 불가
        console.log(arg.skill);
      }
    }
  2. null type은 typeof 시 ‘object’로 반환해버린다는 치명적인 단점이 있습니다


instanceof

instanceof 연산자는, 판별할 객체가 특정한 클래스에 속하는지 확인할 수 있습니다.

💡 사실 JS에서의 class는 prototype이라는 속성을 활용한 것이고, instanceof는 prototype 체인에 생성자의 prototype이 있는지 여부를 확인하는 방식으로 동작하는 것입니다
class Student {
  name: string = 'MINO';
  age: number = 28;
}
class School {
  location: string = 'seoul';
}

function testFunc(arg: Student | School) {
  if (arg instanceof Student) {
    // arg가 반드시 Student 타입으로 인식
    console.log(arg.name);
  } else {
    // arg가 반드시 School 타입으로 인식
    console.log(arg.location); // OK
  }
}
const student = new Student();
testFunc(student);

instanceof 또한 단점이 있습니다

instanceof는 인터페이스에 사용이 불가능합니다. 말 그대로 인스턴스이기 때문에 실제 값으로도 사용이 가능해야 합니다.

그러므로 오로지 클래스에만 사용이 가능합니다

interface Developer {
  skill: string;
}
interface Person {
  age: number;
}
function introduceSelf(arg: Developer | Person) {
  if (arg instanceof Developer) { **// error**
    // 'Developer' only refers to a type, but is being used as a value here.
    console.log(arg.skill);
  }
}

그러므로 typeof 나 instanceof 말고 user defined type guards 를 작성하는 것이 좋습니다



두번째 방법. User Defined Type Guards

is 키워드를 사용해서 특정 프로퍼티의 유무에 따라 type guards를 할 수 있습니다

type guards를 하는 함수는 항상 리턴값으로 우리가 비교하자고자 하는 대상의 프로퍼티에 대해서 true , false값을 리턴합니다

interface Developer {
  name: string;
  skill: string;
}

interface Person {
  name: string;
  age: number;
}

function introduceSelf(): Developer | Person {
  // 인터페이스 타입 Developer | Person 중에서 Person 형태 사용
  return {
    name: 'minho',
    age: 23,
  };
}

// 현재 minho는 Developer | Person 타입이라 name프로퍼티만
// 사용이 가능
const minho = introduceSelf();

// User Defined Type Guards Function
// 인자로 넘어온 값에 skill 프로퍼티가 있다면 arg의 타입을 Developer로 인식
function isDeveloper(target: Developer | Person): target is Developer {
  return (target as Developer).skill !== undefined;
  // return 'skill' in target; 로 사용해도 동일함
}

// type guard를 한 번 거친 후에는 타입에 따른 프로퍼티를 사용 가능
if (isDeveloper(minho)) {
  console.log(minho.skill);
} else {
  console.log(minho.age);
}
💡 아무 조건없이 union type을 사용했을때 공통 프로퍼티만 접근이 가능한 것일 뿐이지 지금은 type guard로 구분하고 있으므로 무조건 name이라는 공통 프로퍼티를 문법적으로 작성할 필요는 없습니다
  • type 캐스팅을 통해 에러를 피한 다음 .skill !== undefined 을 해서 skill프로퍼티가 있는지 확인하고 그 결과를 리턴하는 것입니다
  • 그러면 원래 isDeveloper()함수의 리턴값은 true , false 인 것이지만여기에 리턴 타입으로 is로 type guards를 해준 것이므로
  • isDeveloper함수는 인자로 받은 값에 skill프로퍼티가 있으면 타입을 Developer인터페이스로 평가하게 되고 이제 이 type guard를 한 번 거치고 난 다음에는 타입에 따른 프로퍼티를 사용할 수 있는 것입니다
    if (isDeveloper(minho)) {
      console.log(minho.skill);
    	// 원래는 minho는 여전히 union Type이기 때문에 
    	// (조건문에 상관없이) minho.skill 형태 자체가 불가능했지만
    	// type guards를 거쳤으므로 사용이 가능합니다
    } 
    else {
      console.log(minho.age);
    }

위에서 user defined type guards 함수의 판단조건은 특정 프로퍼티의 유무 였지만 `리턴하는 부분을 조금만 수정하면, 판단 조건을 원하는 대로 바꿀 수 있습니다.`

예를 들어, 특정 객체의 프로퍼티의 값에는 숫자가 오는데, 

특정 숫자일 때 or 아닐 때에 따라 타입을 다르게 구분하고 싶다면 아래와 같이 작성해볼 수 있습니다.

interface ZeroBody {
  age: 0; // 반드시 0만 가능하다는 의미
  name: string;
}

interface OtherBody {
  age: number;
  name: string;
}

interface Response {
  type: string;
  body: ZeroBody | OtherBody;
}

// type guard를 통해 age의 값이 0인 경우에만
// ZeroBody으로 사용되게 함
function isZero(arg: any): arg is ZeroBody {
  return arg.age === 0;
}

function doSomething(arg: Response) {
  const { type, body } = arg;

  if (isZero(body)) {
    console.log(body.age); // 0
  } else {
    // 여기의 body는 OtherBody
    console.log(body.age);
  }
}
profile
웰시코기발바닥

0개의 댓글