TypeScript 8장 - 러닝 타입스크립트(7)
포스트
취소

TypeScript 8장 - 러닝 타입스크립트(7)

TypeScript

image

Part.2

chapter.9 타입 제한자

9.1 top 타입

bottom 타입 개념과 반대되는 top타입
bottom 타입 되돌아보기
  • 가능한 값이 없고, 접근이 불가능한 타입을 나타내기 위한 never타입을 bottom타입이라고도 부른다.
1
2
3
4
5
6
7
8
9
10
11
// bottom 타입의 예제

function throwError(): never{
  throw new Error("Error")
} // 함수가 예외를 던지는 경우 : 반환값이 없다.

function infiniteLoop(): never{
  while(true){
      ...
  }
} // 무한 루프를 발생시키는 경우 : 반환값이 없다.
void도 반환값이 없지 않나?
  • nevervoid의 차이, never타입은 함수가 항상 예외를 던지거나 무한 루프에 빠져서 반환값이 없는 경우이고 void는 함수가 단순히 값을 반환하지 않는 타입이다.

    1
    2
    3
    4
    5
    6
    7
    
    function exampleVoid(num: number): void {
      console.log(num);
    } // return 값이 없는 void
    
    function exampleNever(): never {
      throw new Error("error");
    } // 반환할 수 없는 함수인 never
    
그래서 bottom 타입과 다른 top 타입이라는 건 뭐야?
  • 모든 값의 타입에 알 수 없는 unknwon 타입을 말한다.
  • unknown 타입은 모든 값의 타입에 대해 알 수 없는 상태를 나타내는 타입이며 any타입과 유사하지만 unknown타입이 더 안전한 타입 체크를 제공한다.
any 타입 되돌아보기
  • any타입은 어떠한 타입도 올 수 있기 때문에 타입 검사를 수행하지 않는 것과 같다.
  • 따라서 타입스크립트에서 오류가 발생하지 않더라도 런타임 환경에서는 오류가 발생할 가능성이 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    function objectKey(obj: any, key: string) {
      return obj[key];
    }
    // 입력받은 객체의 key값 알아내기
    let obj = { name: "choi", age: 28 };
    let getName = objectKey(obj, "name"); // OK
    
    // 입력받은 obj는 any타입이기 때문에 객체가 아니어도 오류가 아니다.
    obj = "Error";
    let getError = objectKey(obj, "name"); // OK
    // 객체가 아니기 때문에 'name' 값을 얻을 수 없고, 런타임에서 오류가 발생한다.
    
unknown이 더 안전한 이유는 뭘까
  • 타입스크립트에서 unknown을 사용할 경우, 추가적인 타입 체크를 요구하기 때문에 반드시 타입 체크를 수행해야 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    function exampleUnknownError(value: unknown) {
      return value.toString();
      // Error: 'value' is of type 'unknown'.
      // value는 unknown 타입이기 때문에
      // 추가적인 타입 체크가 필요하다.
    }
    
    function exampleUnknownCheck(value: unknown) {
      // Error: not all code path return a value
      if (typeof value === "number") {
        return value.toString();
      }
      // 타입체크는 했지만 number 타입이 아닐 때에
      // return이 없다.
    }
    
    function exampleUnknown(value: unknown) {
      if (typeof value === "number") {
        return value.toString();
      }
    
      return "";
    }
    

9.2 타입 서술어

타입 서술어가 필요한 이유
1
2
3
4
5
6
7
8
9
10
11
12
13
function numberOrString(value: unknown) {
  return ["number", "string"].includes(typeof value);
}

function useFunction(value: number | string | null | undefined) {
  if (numberOrString(value)) {
    value.toString();
    // numberOrString이 true일 때만 toString()
    // 을 실행하기 때문에 문제가 없지 않을까?
  } else {
    console.log("value does not exist:", value);
  }
}
  • 우리의 의도는 함수 numberOfString을 사용하여 true일 때만 toString()이지만, 타입스크립트는 위의 함수를 참고하여 number,string일 때만 toString()을 사용한다고 생각하지 않는다.
  • 그저 value값의 가능성만 확인하여 위의 함수가 true이든 false이든 상관없이 어쨌든 null과 undefined는 toString()을 사용할 수 없다고 판단하기 때문에 오류가 발생한다.
is 사용하여 타입 좁히기
  • 타입스크립트에서 어떠한 타입을 가져야 한다는 것을 is를 통해 나타낼 수 있다.
  • 타입 가드 함수란, 타입스크립트에서 타입을 정확하게 추론하기 위한 방법이며 boolean타입을 갖는다.
    • 단순히 boolean을 갖는 것이 아니라 더 구체적인 타입임을 나타낸다.
1
2
3
4
5
6
7
8
9
function exampleIs(value: unknown): value is number | string {
  return ["number", "string"].includes(typeof value);
}

function exampleIsError(value: unknown): value is number | string {
  return value;
  // Error: Type 'unknown' is not assignable to type 'boolean'
  //  타입 가드는 boolean 타입을 나타내야 한다.
}
인터페이스 검사하기
  • 인터페이스로 알려진 객체가 더 구체적인 인터페이스의 인스턴스인지 여부를 검사한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    interface Comedian{
      funny: boolean;
    }
    
    interface StandupComedian extends Comedian{
      routine: string;
    }
    
    function isStandupComedian(value: Comedian): value is StandupComedian{
      return 'routine' in value;
    }
    
    function workWithComedian(value: Comedian){
      is(isStandupComedian(value)){
          console.log(value.routine) // OK
      }
    
      console.log(value.routine);
      // Error: Property 'routine' does not exist on type 'Comedian'.
    }
    
    • 인터페이스 Comedian은 routine을 갖고 있을 수도 없을 수도 있는데, routine이 있다면 StandupComedian으로 간주가 된다.

9.3 타입 연산자

9.3.1 keyof
  • 자바스크립트에서는 일반적으로 string타입인 key를 사용하여 객체의 값을 찾지만, 타입스크립트의 string타입은 포괄적인 의미를 갖기 때문에 유효하지 않은 키가 허용될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
interface Ratings {
  audience: number;
  critics: number;
}

function getRating(ratings: Ratings, key: string): number {
  return ratings[key];
  // Error: Element implicitly has an 'any' type because expression
  // of type 'string' Can't be used to index type 'Ratings'
  // No index signature with a parameter of type 'string' was found on type
  // 'Ratings'.
}
  • getRating(ratings, 'errorExample') 처럼 ratings에 들어있지 않는 값에 대해 허용될 수 있기 떄문에 오류가 발생한다.
해결 옵션
  1. 리터럴 유니언 타입을 사용하여, 존재하는 키로 제한한다.

    1
    2
    3
    
    function getRating(ratings: Ratings, key: "audience" | "critic"): number {
      return ratings[key];
    }
    
  2. keyof 연산자를 사용하여 모든 키의 조합을 사전에 정의한다.

    1
    2
    3
    
    function getRating(ratings: Ratings, key: keyof Ratings): number {
      return ratings[key];
    }
    
9.3.2 typeof
  • typeof연산자는 제공되는 값의 타입을 반환한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    const original = {
      medium: "movie", // string
      title: "Mean Girls", // string
    };
    
    let example = typeof original;
    
    let sameExample: {
      medium: string;
      title: string;
    };
    
  • 변수로 선언된 객체와 인터페이스 객체는 무슨 차이일까?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    interface Literal {
      key1: "value";
      key2: "value";
    }
    
    let literal = {
      key1: "value",
      key2: "value",
    };
    
    let example1: typeof literal;
    let example2: Literal;
    
    • 변수 literal은 객체 리터럴로 생성된 값이며, 인터페이스 Literal은 객체의 타입을 정의한다.
    • 똑같은 keyvalue 값을 갖고 있지만 인터페이스는 문자열 value로 고정되어 있어, 이 외의 문자열은 할당할 수 없다.
  • keyof 타입 연산자와 typeof 타입 연산자를 함께 사용해서, 값의 타입에 허용된 키를 간결하게 검색할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    
    const example = {
      key: "examle",
    };
    
    function exampleFun(key: keyof typeof example) {
      return example[key];
    }
    
    • 오직 example에 있는 key만 찾는다.

9.4 타입 어서션

  • 타입스크립트 안에서 타입을 미리 가정하는 것이다.
  • 타입 어서션은 해당 문이 언제나 이라고 간주하기 때문에, 타입스크립트에서는 에러가 발생하지 않아도 자바스크립트에서는 에러가 발생할 수 있다.
  • 무슨 말이냐
    1
    2
    3
    4
    5
    6
    
    const num: number = 1;
    // number으로 선언된 num
    console.log((num as unknown as Array<number>).push(1));
    // 의도적으로 number를 가진 배열이라고 알려준다.
    // num은 number이기 때문에 push(1)을 사용할 수 없지만
    // 타입스크립트는 참으로 인식한다.
    
타입 어서션은 언제 사용하나
  • 위 예시처럼 타입 어서션을 쓰는 것을 피하는 것이 좋으나 JSON.parse처럼, 타입스크립트에서 의도적으로 any타입을 반환하는 경우 특정 타입을 알려주기 위해서 사용하거나 타입에 대한 확신이 있을 때만 사용하는 것이 좋다.
타입 어서션은 어떻게 사용하나
  • 타입 어서션은 두 가지 문법으로 표현된다.

    1
    2
    3
    4
    5
    6
    7
    
    // 1. 앵글 브라킷을 사용한다.
    const exampleAssertion: any = "hello";
    const exampleNumToStr: number = (<string>exampleAssertion).length;
    
    // 2. as 를 사용한다.
    const exampleAssertion: any = "hello";
    const exmapleNumToStr: number = (exampleAssertion as string).length;
    
    • 앵글 브라킷은 React의 JSX문법과 같다고 인식되기 때문에 되도록 as를 쓰는 것이 좋다.
9.4.1 포착된 오류 타입 어서션
  • API를 사용할 때 반환값의 타입을 알고 있지만 해당 타입을 타입스크립트가 추론하지 못하는 경우가 있을 수 있다.
    • 이때 타입 어서션을 사용하여 해당 타입을 좁혀 처리할 수 있다.
  • 외부 라이브러리나 모듈을 사용할 때 해당 모듈의 타입을 인식하지 못하는 경우가 있는데, 해당 모듈에 대한 타입도 정의할 수 있다.
  • try...catch문에서 발생한 Error 객체에는 다양한 속성과 메서드를 가지고 있는데, 이 중 message가 가지고 있는 속성의 정보에 접근할 수 있게 한다.
    • 어떤 타입을 가지고 있는지 명시적으로 지정하여 코드 안정성을 높여준다.
굳이 왜 error에 어서션을 사용해야 할까
  • Error 객체는 unknown이다.
  • 우리가 Error에서 뭔가를 꺼내오고 싶거나, 에러가 어디서 발생했는지 확인하기 위해 Error의 내용을 확인해야 되는 경우가 생기는데, 이때 Error는 unknown이기 때문에 어서션을 사용해야 한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    try {
      throw new Error("Error");
    } catch (error) {
      (error as Error).message;
    }
    
    // 에러가 메세지를 안 갖고 있는 경우, 에러를 찍었는데 그게 또 에러일 수 있다.
    // 또한 에러가 어떤 에러인지 추론할 수 없기 때문에 조건문을 사용하면 좋다.
    try {
      throw "Error";
    } catch (error) {
      (error as Error).message;
      // "Error"자체가 Error이기 때문에 message를 찍는 것 자체가 에러이다.
    }
    
    // 조건문을 활용한 예시
    try {
      axios.put("https://naver.com");
    } catch (error) {
      if (error instanceof AxiosError) {
        console.error("Axios Error >>", error.code);
      }
      if (error instanceof Error) {
        console.error("Error >>", error.message);
      }
    }
    
9.4.2 non-null 어서션
  • 단언 연산자라고도 부르며, 피연산자가 null,undefined라고 아니라고 단언해준다.
non-null 어서션 필요 상황
  • 우리가 사용하고자 하는 요소나 값을 타입스크립트가 추론하지 못 하기 때문에 null을 반환을 한다.
  • non-null이 없다고 단언하기 때문에 타입스크립트의 안쩡성을 해칠 수 있어 권장되진 않는다.
1
2
3
4
5
6
7
8
9
10
11
const button = document.querySelector("button");
// const button: HTMLButtonElement | null
// 이런 상황에서 버튼이 있는지 확인을 해줘야 한다.
if (button) button;
// 이런 상황에서 버튼이 있는지 확인하지 않고
// non-null 어서션을 사용하면
//button! 있다고 가정되어 바로 사용이 가능하다.
button!.textContent = "";
// 만약 어서션을 사용하지 않을 시
button.COMMENT_NODE;
// ERROR : 'button' is possibly 'null'.
9.4.3 타입 어서션 주의 사항
  • any 타입을 사용할 때처럼 꼭 필요한 경우가 아니라면 가능한 사용하지 말아야 한다.
  • 타입스크립트에서는 아무런 오류가 없지만 런타임 오류가 발생할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    const seasonCounts = new Map([
      ["Broad City", 5],
      ["Community", 6],
    ]);
    
    const knownValue = seasonCounts.get("I Love City")!;
    // non-null 어서션 사용
    // undefined 값을 임의로 없다고 가정
    console.log(knownValue.toUpperCase());
    // Runtimes TypeError: Cannot read property 'toUpperCase' of undefined
    
어서션 vs 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Entertainer {
  acts: string[];
  name: string;
}

const exampleRight: Entertainer = {
  name: "need acts",
  // Error: Property 'acts' is missing in type
  // 인터페이스 Entertainer를 참조하기 때문에 acts 프로퍼티가 필요하다.
};

const exampleWrong = {
  name: "need acts",
} as Entertainer;
// 인터페이스 Entertainer를 따른다고 명시적으로 나타내는 것 자체에 문제가 없다.
// exampleWrong이 인터페이스를 따르는 속성과 값을 가진다고 가정한다.
어서션 할당 가능성
  • 타입 중 하나가 다른 타입에 할당 가능한 경우에만 타입 어서션이 허용된다.
  • 완전히 서로 관련이 없는 타입 사이에는 타입 오류를 감지한다.
    1
    2
    3
    4
    
    let assertion = "Example" as number;
    const exampleWrong = 1 as Entertainer;
    // Error
    const exampleWrong = 1 as unknown as Entertainer; // OK
    

9.5 const 어서션

  • const어서션은 모든 값을 상수로 취급해야 함을 나타낼 때 사용한다.
    • 배열은 가변 배열이 아니라 읽기 전용으로 취급한다.
    • 리터럴은 일반적은 원시 타입(string,number)와 동일하지 않고 리터럴로 취급한다.
    • 객체의 속성은 읽기 전용으로 간주된다.
  • 코드의 가독성과 유지보수성을 높이고, 에기치 않은 값 변경으로 인한 오류를 방지하기 위해 사용한다.

    1
    2
    3
    4
    5
    
    const myNumber: number = 0;
    // myNumber의 값은 숫자인 0으로 고정된다.
    let exampleNum = 10 as const;
    exampleNum = 0;
    // Error: Type '0' is not assignable to type '10'.
    
9.5.1 리터럴에서 원시 타입으로
  • 함수에 대해 특정 리터럴이 오도록 타입스크립트에게 알려줄 수도 있고, 구체적인 값을 지정할 수도 있다.
  • 왜 하냐, 예기치 않은 값이 올 때를 방지하여 안정성을 높이기 위해 사용한다.

    1
    2
    3
    4
    
    const getName = () => "Maria Bamford";
    // "Maria Bamford"를 리턴하는 함수 getName에게
    const getName = () => "Maria Bamford" as const;
    // "Maria Bamford"만을 리턴할 것이라고 값을 좁혀준다.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    interface Joke {
      quote: string;
      style: "story" | "one-liner";
    }
    
    function tellJoke(joke: Joke) {
      // 변수 joke는 Joke인터페이스를 참조한다.
      if (joke.style === "one-liner") {
        console.log(joke.quote);
      } else {
        console.log(joke.quote.split("|n"));
      }
    }
    
    const narrowJoke = {
      quote: "too long sentence",
      style: "one-liner" as const,
    };
    
    tellJoke(narrowJoke); // OK
    // 왜 OK일까
    
    const wideObject = {
      quote: "also too long sentence",
      style: "one-liner",
      // string 타입이기 때문에
    };
    
    tellJoke(wideObject);
    // Error: Argument of type "{quote: string; style: string;}"
    // is not assignable to parameter of type "LogAction"
    // Types of propery 'style' are incompatible.
    // Type 'string' is not assignable to type "story"|"one-liner".
    
9.5.2 읽기 전용 객체
  • as const를 사용해 리터럴을 어서션하면 유추된 타입이 구체적으로 전환된다.
  • 고유한 리터럴 타입으로 간주되며, 배열은 읽기 전용 튜플이 된다.
  • 리터럴에 const 어서션을 적용하면 해당 리터럴의 모든 멤버에 동일한 const어서션이 재귀적으로 적용된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    function describePreference(prefenrence: "maybe" | "no" | "yes") {
      switch (prefenrence) {
        case "maybe":
          return "I suppose...";
        case "no":
          return "No thanks.";
        case "yes":
          return "Yes plz";
      }
    }
    
    const preferencesMutable = {
      movie: "maybe",
      standup: "yes",
    };
    
    describePreference(prefenrencesMutable.movie);
    // Error: Argument of type 'string' is not assignable to
    // parameter of type '"maybe"|"no"|"yes"'.
    
    prefenrencesMutable.movie = "no"; // OK
    
    const preferencesReadonly = {
      movie: "maybe",
      standup: "yes",
    } as const;
    
    decrementPreference(prefenrencesReadonly.movie); // OK
    
    prefenrencesReadonly.movie = "no";
    // Error: Cannot assign to 'movie' because it is a readonly property.
    
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.