타입 추론, 타입 단언, 타입 가드, 타입 호환 개념 확실히 알아보기!

신은수·2023년 11월 13일
2

TypeScript

목록 보기
2/2
post-thumbnail

1. 타입 추론

1) 타입 추론이란?

  • 타입스크립트가 코드를 해석하여 적절한 타입을 정의하는 동작을 의미한다. 변수를 하나 선언하고 값을 할당하면 해당 변수의 타입은 자동으로 추론된다.

2) 타입추론을 적극 활용하자

  • const a = '5' : a: '5' 라고 추론한다.
  • const a: string ='5' : 타입을 지정해줬지만 오히려 이것은 잘못된 타입 지정이다. 왜냐하면 const로 선언했기 때문에 값이 바뀔 일이 없는데 오히려 '5'에서 string이라는 넓은 타입으로 타입을 지정한 꼴이 되었다.
  • 따라서 타입을 지워보고, 타입추론을 이상하게 하면(any이거나 틀린 타입일때) 타입을 다시 쓰면 된다. 항상 마우스를 올려서 타입추론을 적극 활용하자.

2. 타입 단언

1) 타입 단언이란?

  • 타입 스크립트의 타입 추론에 기대지 않고 개발자가 직접 타입을 명시하여 해당 타입으로 강제하는 것을 의미한다.
  • 타입 단언은 숫자, 문자열, 객체 등 원시 값 뿐 아니라 변수나 함수의 호출 결과에도 사용가능
    function getId(id){
    	return id;
    }
    
    var myId = getId('josh') // myId는 any로 추론됨
    var myId2 = getId('josh') as string // myId는 string으로 추론됨

2) 타입 단언을 사용할 때 주의해야할 점

  • as 키워드는 구문 오른쪽에서만 사용한다.
    var num as number = 10; // (x)
    var num = 10 as number; // (o)
    var num: number = 10;
  • 호환되지 않는 데이터 타입으로는 단언할 수 없다
    • 'number' 형식을 'string' 형식으로 변환하는 작업은 실수입니다. 두 형식이 충분히 겹치지 않기 때문입니다. 의도적으로 변환한 경우에는 먼저 ‘unknown’으로 식을 변환합니다.
      var num = 10 as string; // (x) 
      
      var num = (10 as any) as string; // (o)
  • 타입 단언을 남용하지 않기: 타입 단언은 코드를 실행하는 시점에서 아무런 역할도 하지 않기 때문에 에러에 취약하다. 타입 에러는 해결되지만 실행 에러는 미리 방지하지 못한다.

3) null 아님 보장연산자(non null assertion): !

  • 타입 단언의 한 종류로 as 키워드와는 용도가 다르다. 값이 null이 아님을 보장해준다.
  • non null assertion 쓰기전
    const head = document.querySelector("#head");
    if (head) {
      head.innerHTML = "hi";
    }
  • non null assertion 쓰면?

    const head = document.querySelecotr('#head')!
    head.innerHTML = "hi";
  • non null assertion은 최대한 쓰지말자 (타입 에러는 해결되지만 실행 에러는 미리 방지하지 못한다.)

    만약에 다른 개발자가 html코드에서 id head를 header로 바꾸면 어떻게 될까?


3. 타입 가드

1) 타입 가드란?

  • 여러 개의 타입으로 지정된 값을 특정 위치에서 원하는 타입으로 구분하는 것을 의미한다. 타입 시스템 관점에서는 넓은 타입에서 좁은 타입으로 타입 범위를 좁힌다는 것을 의미한다.

2) typeof

  • 타입단언(as)으로 해결했을 때 문제점
    • 타입단언(as)으로 a의 타입이 number라고 안심시켰지만, 런타임에서 문자열을 함수의 인자로 넣었을 때, 결국 에러가 난다.
      function numOrStr(a: number | string){
       (a as number).toFixed(2); // 위험한 코드임. a가 number라고 TS를 안심시키는 행위.
      }
      **numOrStr('123.1111');** // 에러난다. 타입스크립트를 number라고 안심시켰지만, 결국 문자열이기 때문에 런타임에서 에러가 난다.
      **numOrStr(123.1111);**  
  • typeof로 타입 좁히기
    function numOrStr(a: number | string) {
      if (typeof a === "number") {
        console.log(a.toFixed(2));
      } else if (typeof a === "string") {
        console.log(a.charAt(2));
      }
    }
    
    numOrStr(123.1111);
    numOrStr("123.1111");

3) Array.isArray (배열로 타입좁히기)

function numObjOrNumArr(a: number[] | object) {
  if (Array.isArray(a)) {
    console.log(a.concat(4));
  } else {
    Object.entries(a).forEach(([key, value]) => {
      console.log(key, value);
    });
  }
}

numObjOrNumArr({ one: 1, two: 2, three: 3 });
numObjOrNumArr([1, 2, 3]);

4) instance of (클래스의 타입 좁히기)

class A {
  aaa() {}
}
class B {
  bbb() {}
}

function aOrB(param: A | B) {
  if (param instanceof A) {
    param.aaa();
  }
}
aOrB(new A());

5) in 연산자 (객체의 타입 좁히기)

type B = { type: "e"; eee: string };
type F = { type: "f"; fff: string };
type G = { type: "f"; ggg: string };

function typeCheck(a: E | F | G) {
  if (a.type === "e") {
    console.log(a.eee);
  } else {
    if ("fff" in a) {
      console.log(a.fff);
    } else {
      console.log(a.ggg);
    }
  }
}

6) 타입가드 함수

  • 타입 가드 함수란 타입 가드 역할을 하는 함수를 의미한다. 주로 객체 유니언 타입 중 하나를 구분하는데 사용하며, in 연산자와 역할은 같지만 좀 더 복잡한 경우에 사용한다.
    interface Cat {
      meow: number;
    }
    interface Dog {
      bow: number;
    }
    
    function isDog(a: Cat | Dog): a is Dog {
      return (a as Dog).bow !== undefined;
    }
    
    function greet(animal: Cat | Dog) {
      if (isDog(animal)) {
        console.log("개가 짖는다.");
        for (let i = 0; i < animal.bow; i++) {
          console.log("왈");
        }
      } else {
        console.log("고양이가 운다");
        for (let i = 0; i < animal.meow; i++) {
          console.log("냐옹");
        }
      }
    }
  • 실전예제로 이해하기
    const isRejected = (
      input: PromiseSettledResult<unknown>
    ): input is PromiseRejectedResult => input.status === "rejected";
    const isFulfilled = <T>(
      input: PromiseSettledResult<T>
    ): input is PromiseFulfilledResult<T> => input.status === "fulfilled";
    
    // Promise -> Pending -> Settled(Fullfilled, Rejected)
    const promises = await Promise.allSettled([
      Promise.resolve("a"),
      Promise.resolve("b"),
    ]);
    
    const errors1 = promises.filter((a) => true);
    const errors2 = promises.filter((promise) => promise.status === "rejected");
    const errors3 = promises.filter(isRejected); 
    export {};
    • const errors1 = promises.filter((a) => true);: PromiseSettledResult[] 로 추론
    • const errors2 = promises.filter((promise) => promise.status === "rejected");: 타입가드 안쓰면 이런식으로 코딩해야한다.
      하지만 아직도 TS는 errrors2를 PromiseSettledResult[]로 추론한다. 따라서, 에러나는 것만 구별하고 싶으면 isRejected 타입가드를 쓰고, 성공하는 것만 걸러내고 싶으면 isFullfilled 타입가드를 쓴다.
    • const errors3 = promises.filter(isRejected); : 타입가드를 쓰지 않으면 에러인지 성공인지 구별이 제대로 되지 않는다. 드디어 TS는 errorr3을 PromiseRejectedResult[]로 추론한다.

4. 타입 호환

1) 타입 호환이란?

  • 서로 다른 타입이 2개 있을 때 특정 타입이 다른 타입에 포함되는지를 의미한다. 쉽게 말하면 타입 간 할당 가능 여부로 ‘타입이 호환된다’ 혹은 ‘호환되지 않는다’고 표현
    var a: string = "hi"
    var b: number = 10;
    b = a; // 에러남. 'string' 형식은 'number' 형식에 할당할 수 없음
    
    var a: string = "hi"
    var b: "hi" = "hi";
    b = a; // 에러남. 'string' 형식은 'hi' 형식에 해당할 수 없음
    
    a = b; // 에러안남
    

2) 다른 언어와 차이점

  • 타입스크립트의 타입 호환이라는 개념은 다른 언어와 차이가 있다.
    interface IHero {
      name: string;
    }
    
    class CHero {
      name: string;
      constructor(name: string) {
        this.name = name;
      }
    }
    
    let i: IHero;
    i = new CHero("ironman");
    → 이런 코드에서 타입 에러가 발생하지 않는 이유는 타입스크립트의 ‘구조적 타이핑’ 특성 때문이다
  • 구조적 타이핑: 타입 유형보다는 타입 구조로 호환 여부를 판별하는 언어적 특성

3) 타입을 집합으로 생각하자(좁은타입과 넓은타입)

  • (any는 전체집합, never는 공집합)
  • C가 가장 좁은 타입
    type A= {name: string}
    type B = {age: number}
    type C = {name: string, age: number}
  • 좁은 타입에 넓은 타입을 대입하면 에러난다. 넓은 타입에 좁은 타입을 대입하면 에러가 나지 않는다.
    type A= {name: string}
    type B = {age: number}
    type C = A | B // C는 D보다 넓은타입
    type D = A & B // D는 C보다 좁은 타입
    
    // D는 C보다 좁은타입
    // D의 타입을 가진 d에 C의 타입을 가진 c를 대입.
    // 즉 좁은 타입에 넓은 타입을 대입하여 에러가 남.
    const c: C = {name: 'zerocho'} 
    const d: D = c  
    
    // C는 D보다 넓은 타입
    // C의 타입을 가진 c에 D의 타입을 가진 d를 대입.
    // 즉 넓은 타입에 좁은 타입을 대입하여 에러가 나지 않음.
    const d: D = {name: 'eunsu', age: 25}  
    const c: C = d 
  • 객체 리터럴 검사, 잉여 속성검사
    const d:D = {name: 'zerocho', age: 29, married: false} -> 에러남
    // 넓은 타입에 좁은 타입 대입하면 에러 안난다며 ?!! 
    
    const obj = {name: 'zeorcho', age: 29, married: false}
    const d:D = obj; // 에러 안남
    • 에러나는 이유: 객체 리터럴 검사라는 것이 생겨서 객체 리터럴을 바로 넣으면 잉여속성검사라는 것을 하기 때문에
profile
🙌꿈꾸는 프론트엔드 개발자 신은수입니당🙌

0개의 댓글