TIL66.타입 추론&타입 단언&타입 가드&타입 호환

조연정·2021년 1월 14일
0
post-thumbnail

타입스크립트가 타입을 추론을 해나가는 과정 및 단언과정 등에 대해 알아보자.

Type Inference

타입 추론이란 타입스크립트에서 타입이 지정되어 있지 않은 경우, 코드를 해석하여 타입을 유추해나가는 동작을 말한다.
일일이 변수를 선언할 때마다 타입을 선언해야하고 필요한 타입을 정의해야하는 번거로운 문제를 생각할 수 있지만, 타입 추론이 얼마나 잘 되느냐에 따라 타입 선언 비용이 줄어든다.

타입 추론 예제 1)

//index.ts

let a = 10;
a = 'str' //error

처음으로 'a'를 선언해줬을 때, 숫자를 할당해줬기 때문에 타입스크립트는 'a'를 숫자 타입으로 추론했다. 'a'를 숫자 타입이 아닌 'str'문자 타입으로 재할당해줬기 때문에 타입에러가 발생한다.

타입 추론 예제 2)


'DetailedDropdown'stirng 타입이 제네릭 < T2 >로 연결되고, 그 타입이 'DropdownInterface'제네릭 타입으로 들어와서 궁극적으로 DropdownInterface에 string 타입이 들어오게 되었다.

Best Common type

말그대로 가장 일반적인 타입이다. 여러가지 자료형이 배열 내에서 사용되고 있을 때, 그 여러가지 자료형을 포괄할 수 있는 가장 일반적인 자료형을 추론하는 것이다. 대부분의 경우 유니온 타입으로 추론된다.

const arr = [1,2,true,'a'];
// number | boolean | string 타입이 추론된다.

Type Assertion

타입 단언은 ts컴파일러가 타입을 실제 런타임에 존재할 변수의 타입과 다르게 추론하거나 너무 보수적으로 추론하는 경우에 프로그래머가 수동으로 특정 변수에 대한 타입을 강제로 부여하는 것을 말한다.

let a; // let a: any

a = 'abc'
a = 10

let b = a as number; // 내가 너보다 이 타입을 잘 아니까 내가 정한 타입으로 하자.

타입 단언이 실제로 많이 사용되는 곳은 Dom API(querySelector...)를 조작할 때 많이 사용된다.

const myInp = document.querySelector('.myInp');  
myInp.innerText= "..." 
//// error: property 'innerText' dose not exist on type 'HTMLElement'.

위 코드에서 변수 'myInp'은 myInp이라는 클래스가 있다고 보장해주지 않는다. 변수'myInp'은 HTMLInputElement | null 이라는 타입을 보장받을 뿐이다. null일수도 있기때문에 null이 아닌 것을 확인시켜줘야한다.

const myInp = document.querySelector('.myInp') as HTMLInputElement;  

Type Guard

함수의 파라미터로 유니온 타입이 지정되는 경우 공통되는 속성만 사용이 가능하다. 이럴 경우 각 타입을 분기 처리하여 타입별로 로직을 분리하기 위한 작업이다.

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

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

function introduce() : Magician | Person {
    return {name: 'Tony', age:40, skill:"fire"}
}

const tony = introduce();
console.log(tony.name)  
console.log(tony.skill) //error

// 타입 단언 사용하여 skill 사용
if((tony as Magician).skill) {
console.log(((tony as Magician).skill))
} else if ((tony as Person).age) {
    console.log((tony as Person).age)
}

변수 'tony'는 Magician | Person의 공통된 속성만 사용가능하기 때문에 tony.skill 사용불가하다. 타입 단언을 사용해서 skill을 사용할 수 있지만 가독성이 떨어지게된다. 이때 타입가드를 사용하면 좋다.

타입 가드 정의

//타입 가드 정의
function isMagician(target: Magician | Person): target is Magician {
 return (target as Magician).skill !== undefined;
}

//skill, name 사용 가능
if((tony as Magician).skill) {
console.log(((tony as Magician).skill))
} else if ((tony as Person).age) {
    console.log((tony as Person).age)
}

target is Magician 키워드로 넘겨받은 파라미터가 해당 타입인지를 확인한다. skill이 있다면 Magicain 타입


Type Compatibility

타입 호환이란 타입스크립트 코드에서 특정 타입이 다른 타입에 잘 맞는지를 의미한다.

interface타입 호환

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

interface Person {
    name: string;
}

let developer: Dev;
let person: Person;

developer = person; //error

할당해주는 변수의 속성들이 왼쪽의 변수에 속하는 일종의 교집합 형태(Dev>Person)이기때문에 person에는 skill 속성이 없어서 오류가 발생한다.

함수 타입 호환

let add = function(a:number): any {
    console.log(a)
}

let sum = function(a:number, b:number): number {
    return a+b
}

sum = add; // o
add = sum; // x

sum이라는 함수의 구조가 add함수의 구조보다 크기 때문에 sum 함수를 add함수에 할당해줄 수 없다.

제네릭 타입 호환

interface Empty<T> {

}

let empty1: Empty<string>;
let empty2: Empty<number>;
empty1 = empty2;  //o
empty2 = empty1;  //o

interface NotEmpty<T> {
    data: T;
}

let notempty1: NotEmpty<string>;
let notempty2: NotEmpty<number>;
notempty1 = notempty2;  //err
notempty2 = notempty1;  //err

제네릭 타입 간의 호환 여부를 판단할 때 타입 인자 < T >가 속성에 할당되었는지를 기준을 판단한다. 위에 코드처럼 인터페이스에 속성이 있어서 제네릭의 타입 인자가 속성에 할당된다면 에러가 발생한다.

* 이외에 enum타입은 number타입과 호환되지만, 이넘타입끼리는 호환되지 않는다.

*추가) 실제 사례

프로젝트를 진행하다 타입추론에 관한 오류가 한가지 발생했다. 타입 단언을 포함한 몇가지 해결방법이 있어서 기록하고 싶었다.

// 자세한 전체 코드는 생략
const IMG = [
  { src: checklist, alt: 'check' },
  { src: circle, alt: 'complete' },
  { src: file, alt: 'attachment' },
];
const organizeData = useMemo(() => {
    return WashInformationType.allCases.map((type, i) => [type.name, summaryOfWash?.count?.[i]?.value, IMG[i]]);
  }, [summaryOfWash?.count]);
      ...
      {organizeData.map(([key, value, img]) => {
          return (
            <span>
              <img src={img.src} alt={img.alt} />
              {key} : {value}</span>
          );
        })}

=> Property 'src' does not exist on type 'string | number | IMGType'.   Property 'src' does not exist on type 'string'.
튜플의 형태로 데이터를 정리해서 맵을 돌리게 코드를 작성했다.
튜플의 첫번째 요소 type.name은 string, 두번째 요소 value는 number, 세번째 요소 img의 src와 alt는 배열 안의 객체이다.
세 타입의 요소가 한 배열 안에 존재하기 때문에, 타입스크립트의 타입추론에 의해서 세 가지 타입을 한꺼번에 가지게 된다. 세번째 요소는 stirng 일수도 number 일수도 있기 때문에 정확한 추론이 불가능해 오류를 발생한다. 이를 해결하기 위해서 몇가지 해결방법이 있다.
i) 타입단언 사용하기

type IMGType = {
  src: string;
  alt: string;
};
const IMG: IMGType[] = [
  { src: checklist, alt: 'check' },
  { src: circle, alt: 'complete' },
  { src: file, alt: 'attachment' },
];

{organizeData.map(([key, value, img]) => {
          return (
            <span>
              <img src={(img as IMGType)?.src} alt={(img as IMGType)?.alt} />
              {key} : {value}</span>
          );
        })}

세번째 요소에 타입을 직접적으로 단언해서 해결해주었다.
사실 타입 단언은 최후의 수단이라고 들어서 지양하는 것이 좋다. 후에 들어올 타입에 대해 사용하기 어려워지기 때문에 '이 타입만 쓴다' 이런 확신이 없다면 최대한 안쓰는 것이 좋다.

ii) 튜플 타입 지정하기

type tuple = [string | undefined, number | null | undefined, IMGType];

const organizeData = useMemo(() => {
    return WashInformationType.allCases.map((type, i) => [type?.name, summaryOfWash?.count?.[i]?.value, IMG[i]] as tuple);
  }, [summaryOfWash?.count]);
      ...
 {organizeData.map(([key, value, img]) => {
          return (
            <span>
              <img src={img.src} alt={img.alt} />
              {key} : {value}</span>
          );
        })}
profile
Lv.1🌷

0개의 댓글