타입스크립트 - 이해하기

드엔트론프·2023년 7월 30일
0

typescript

목록 보기
4/12
post-thumbnail

들어가며

  • 타입스크립트의 기본적인 타입을 알면 끝일까? 아쉽게도 아니다. 기본적인 타입을 처리할 순 있겠지만, 제대로 활용하기엔 어렵다.
  1. 어떤 기준으로 타입을 정의하는지,
  2. 어떤 기준으로 타입간의 관계를 정의하는지,
  3. 어떤 기준으로 타입의 오류를 검사하는지
    를 알고 작성하는 게 중요하다.

타입은 집합이다.

  • 집합의 개념으로 타입을 구분하면, 타입의 관계를 이해하는 데 큰 도움이 된다.

업캐스팅, 다운캐스팅

슈퍼타입, 서브타입

  • 넘버 리터럴 타입은 넘버 타입에 속한다. 그래서 넘버 타입은 슈퍼타입 또는 부모타입이라고 부르고 넘버 리터럴 타입은 서브타입 또는 자식 타입이라고 부른다.
  • 넘버 리터럴 타입에서 넘버 타입에 할당해주는건 업 캐스팅이라고 하며 모든 상황에 가능한 행동이지만,
  • 넘버 타입에서 넘버 리터럴 타입에 할당하는건 대부분 불가능하며, 이는 다운 캐스팅이라 부른다.

타입계층도

  • 위 그림은 타입계층도이다. 앞선 설명과 함께 아래의 예시를 보자.
// unknown 타입 --> 전체 집합

function unknownExam(){
  let a : unknown = 1;
  let b : unknown = 'hello';
  let c: unknown = true;
  let d : unknown = null;
  let e: unknown = undefined;

  let unknownVar : unknown;
  let num : number = unknownVar; // 오류
  let str: string = unknownVar; // 오류
  let bool : boolean = unknownVar; //오류
}
  • a,b,c,d,e는 되는데 num,srt,bool 은 에러가 나는 이유는?
    • 위에서 말했듯 업캐스팅, 다운캐스팅으로 보면 된다. unknown은 계층도에서 보면 알 수 있듯 전체 집합으로, 아래 모든 요소들로 다 바뀔 수 있지만(업캐스팅)
    • number,string,boolean 같은 친구들은 unknown 이 될 수 없다(다운캐스팅)
// never 타입 --> 모든 타입의 서브타입. 모든 타입의 부분집합 = 공집합

function neverExam() {
  function neverFunc(): never {
    while (true) {}
  }
  let num: number = neverFunc();
  let str : string = neverFunc();
  let bool : boolean = neverFunc();

  let never1 : never = 10;  //오류
  let never2: never = 'string'; //오류
  let never3: never = true; //오류

}
  • unknown 예시와 비슷하다. number,string,boolean 타입에 never 할당은 가능하지만 (업캐스팅)
  • never 타입에 number, string, boolean 타입을 줄 순 없다. (다운캐스팅)
// any 타입

function anyExam(){
  let unknownVar : unknown;
  let anyVar : any;
  let undefinedVar : undefined;
  let neverVar : never
  anyVar = unknownVar;
  undefinedVar = anyVar;
  neverVar = anyVar; //오류
}
  • any는 알다시피 엥간함 다된다.. 이 any가 예외상황이 많다. 타입 계층도에서 보면 anyVar = unknownVar; 같은 경우 되면 안된다. 왜냐면 unknown이 더 상위인데 any를 받는 다운캐스팅이니까, 근데 된다..!
  • undefinedVar = anyVar; 도 계층도를 보면 되면 안된다. any가 더 상위인데 undefinedVar에 할당하는건 다운캐스팅이니까, 근데 또 된다...!
  • never에는 안된다. never는 말 그대로 순수한 공집합의 상태이기 때문에 안된다.
  • 이런 예외적인 부분이 있기에 다운캐스팅이 무조건 안된다 한 게 아니라 대부분 안된다 라고 말한 것 같다.

객체의 호환성

  • 책의 타입을 정해주는 게 있다고 해보자.
  • 하지만 프로그래밍 책은 skill 을 추가로 갖고 있다.
type Book = {
  name: string;
  price: number;
}
type ProgrammingBook = {
  name: string;
  price: number;
  skill: string;
}

let book : Book;
let programmingBook : ProgrammingBook = {
  name : "한입",
  price : 33000,
  skill : "react",
}

book = programmingBook
programmingBook = book //오류

객체는 프로퍼티 개수가 더 적은게 상위 타입이다.

  • 그렇기에 programmingBook = book 은 오류가 발생한다.
/**
 * 초과 프로퍼티 검사
 */

let book2: Book = {
  name: "한입",
  price: 33000,
  skill: "react", //오류 발생
};

let book3: Book = programmingBook
  • 여기서 book2는 skill에서 에러가 난다. book3는 되는데? 같은 동작하는건데 뭐는 되고 뭐는 안되고 왜이럴까? 그건 바로 초과 프로퍼티 검사 때문이다. 그렇기에 이 검사를 피하기 위해서는 book3처럼 적던가, type에 정의된 프로퍼티만 써줘야 한다.(약간은 당연하게도)
function func(book: Book) {}

func({ name: "한입", price: 33000, skill: "react" }); //skill 오류 발생
func(programmingBook);
  • 함수의 매개변수도 비슷한 형태로 에러가 난다. 그러니 위의 설명처럼 검사를 피하기 위해서는 func(programmingBook);처럼 적던가, type에 정의된 프로퍼티만 써줘야 한다.

타입 단언

이번엔 이런 상황을 보자.
객체 변수 person을 만드는데 초기값은 굳이 안넣고 빈 객체로 시작하고 싶다.

type Person = {
  name: string;
  age: number
}

let person = {}
person.name = '강' //에러 '{}' 형식에 'name' 속성이 없습니다.ts(2339)
person.age = 27; //에러 '{}' 형식에 'age' 속성이 없습니다.ts(2339)
  • person에 타입을 정의해주지 않으니 에러가 난다. 그럼 Person으로 정의하면?

type Person = {
  name: string;
  age: number
}

let person : Person= {} //에러 '{}' 형식에 'Person' 형식의 name, age 속성이 없습니다.ts(2739)
person.name = '강'
person.age = 27;
  • 이러면 변수에서 에러가 난다. 그럼 방법이 없나 ? any를 넣어줘야하나?
  • 아니지 , 타입 단언을 해준다.
type Person = {
  name: string;
  age: number
}

let person= {} as Person //이렇게. 영어로는 type assertion
person.name = '강'
person.age = 27;


  • 다른 예시를 통해 조금 더 살펴보자



type Dog = {
  name: string;
  color: string;
};

let dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도", //초과 프로퍼티
} as Dog;
  • breed는 초과 프로퍼티에 걸리기에 원래는 에러가 난다. 하지만 as Dog로 타입 단언을 해주면 에러가 사라진다.

    이러한 타입단언은 만족해야 할 규칙이 하나 있는데 바로 A가 B의 슈퍼타입이거나, A가 B의 서브타입이어야한다는 것이다.

/**
 * 타입 단언 규칙
 * 값 as 단언 <- 단언식
 * A가 B의 슈퍼타입이거나
 * A가 B의 서브타입이어야함
 */

let num1 = 10 as never;
let num2 = 10 as unknown;
let num3 = 10 as string // 에러 'number' 형식을 'string' 형식으로 변환한 작업은 
// 실수일 수 있습니다. 두 형식이 서로 충분히 겹치지 않기 때문입니다. 
// 의도적으로 변환한 경우에는 먼저 'unknown'으로 식을 변환합니다.ts(2352) 
  • num3는 에러가 난다. 왜냐면 number 타입과 string 타입은 둘의 관계가 서브타입이나 슈퍼타입이 아니니까.
  • 아주 악랄하게 막는 방법으로는 이런게 있다.
let num3 = 10 as unknown as string; //다중 단언 -> 절대로 좋은 방법은 아님.

const 단언

  • as const 라고 단언하는건데, 그러면 상수가 되는 효과를 볼 수있다.
/**
 * const 단언
 */

let num4 = 10 as const;

let cat = {
  name: "야옹쓰",
  color: "yellow",
} as const;

cat.name = ""; //읽기 전용 속성이므로 'name'에 할당할 수 없습니다.ts(2540)

Non null 단언

  • 처음 보는 기능이다. 일전에 프로젝트를 하며 자주 겪었던 에러다. 너 그거 요소 없어 ~ 에러야 하며 빨간줄을 많이 냈었는데, ! 를 줌으로써 타입스크립트에게, 아냐 이거 있어 ~! 라고 말해주는 것
/**
 * Non null 단언
 */

type Post = {
  title : string;
  author?: string;
}

let post : Post = {
  title: '게시글1',
  author: '정환쓰'
}

//'number | undefined' 형식은 'number' 형식에 할당할 수 없습니다.
//  'undefined' 형식은 'number' 형식에 할당할 수 없습니다.ts(2322)
// const len : number = post.author?.length; 

const len: number = post.author!.length; //!를 줌으로써 non이나 null이 아님을 알려주는것

타입 좁히기

  • 좁히는 방법이 여러가지다. typeof, instanceof, in
/**
 * 타입 좁히기
 * 조건문 등을 이용해 넓은타입에서 좁은타입으로
 * 타입을 상황에 따라 좁히는 방법
 */

type Person = {
  name: string;
  age: number;
}

//value -> number : toFixed
//value -> string: toUpperCase
//value -> Date : getTime
//value -> Person : name은 age입니다.
function func(value: number | string | Date | null | Person) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } 
  // else if (typeof value === 'object'){
  //   console.log(value.getTime())
  // } 
  else if (value instanceof Date){ //왼쪽에 있는 value가 Date 객체니?
    console.log(value.getTime());
  } else if( value && 'age' in value){
    console.log(`${value.name}${value.age}입니다.`)
  }
}
  • 차근차근 보자. 기본적으로 number, string 같은 값들은 typeof로 처리할 수 있다.
  • 그러나 Date 같은 내장 메서드를 typeof value === ‘object' 로 처리하려 하면, 공포의 빨간줄이 그어진다. 왜냐면 매개변수에 null로도 받을 수 있는데, object가 null일 수 있기 때문이다.
else if (typeof value === 'object'){
    console.log(value.getTime()); // 오류 'value'은(는) 'null'일 수 있습니다.
  }
  • 그렇기에 instanceof 라는 타입 가드를 쓰게 되는데, 이건 쉽게 말해 왼쪽에 있는 value가 Date객체 맞니 ? 하는 것이다.
else if (value instanceof Date){ //왼쪽에 있는 value가 Date 객체니?
    console.log(value.getTime());
  • 그런데 내가 정의한 타입을 받는 친구는 어떻게 타입가드를 해줘야 할까? 바로 in 을 써주면 된다.
else if( value && 'age' in value){
    console.log(`${value.name}${value.age}입니다.`)
  }
  • ‘age’는 내가 정의한 Person에만 있는 타입이므로 value안에 age가 있다면~ 으로 타입을 좁혔는데, 참고할점은 앞에 value && 을 붙였다는점이다. value라는 값이 있을 때, 를 붙여주지 않으면 value가 null일수도 있기에 빨간줄이 그어진다.

서로소 유니온 타입도 함께 적으려 했는데 내용이 조금 있기도하고, 따로보면 더 좋을 것 같아 다음 글에 따로 작성해보려한다.

profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

0개의 댓글