[한입챌린지]타입스크립트 DAY6

Lina Hongbi Ko·2025년 3월 12일
0
post-thumbnail

DAY6

2025년 3월 3일

✨ 타입 단언

  • 타입 단언은 실제로 값을 그 타입으로 바꾸는 것은 아님(업캐스팅/ 다운캐스팅이 아님)

    • 타입스크립트 컴파일러의 눈을 잠깐 가리는 용도임
    • 따라서, 타입 단언을 사용할 때는 확실할 때에만 사용해야함
  • 빈 객체로 초기화 후, '.연산자'를 사용해서 프로퍼티를 추가하면 오류 발생

  • 타입 정의를 해서 문제가 되는 것 같아 타입을 지정하지 않아도 빈 객체를 기준 값으로 추론되기 때문에 그냥 빈 객체가 변수의 타입이 됨

    /**
     * 타입 단언
     */
    type Person = {
      name: string;
      age: number;
    };
    let person = {};
    person.name = '이정환'; // 오류 발생
    person.age = 27; // 오류 발생
  • 그럼 어떻게 해야할까?

    • 이럴 때 사용하는 것이 '타입 단언'
    • 의도와 다르게 변수의 타입이 추론 되어서 원하는 기능을 만들기 어려울 때 초기화 값을 단언 해줌
    • 값 뒤 as 키워드 사용하고 타입을 명시 -> 값을 타입스크립트 컴파일러에게 as 뒤에 사용한 타입으로 간주하라고 알려줌
  • 값의 타입을 프로그래머가 직접 단언 하는 방법 = 타입 단언(type assertion)

    • 타입 단언된 초기화 값을 기준으로 타입 추론이 이루어짐
     /**
     * 타입 단언
     */
      type Person = {
        name: string;
        age: number;
      };
      let person = {} as Person;
      person.name = '이정환';
      person.age = 27;
  • 다른 경우를 살펴보자.

    • 객체의 프로퍼티를 하나 더 추가하면 어떻게 될까?
  • 변수를 초기화할 때, 객체 리터럴로 초기값을 설정하면 초과 프로퍼티 검사가 발동됨 -> 추가 프로퍼티를 허용하지 않음 -> 그런데 어쩔 수 없이 추가 프로퍼티를 꼭 넣어야한다면?

    • 타입 단언 사용
    type Dog = {
      name: string;
      color: string;
    };
    
    let dog: Dog = {
      name: '돌돌이',
      color: 'brown',
      breed: '진도' // 오류 발생
    }
    • 타입 단언 사용
      • 객체 값을 타입 단언을 사용해줬기 때문에 굳이 타입 정의를 하지 않아도 됨 -> 자동으로 타입 추론함
    type Dog = {
      name: string;
      color: string;
    };
    
    let dog = {
      name: '돌돌이',
      color: 'brown',
      breed: '진도'
    } as Dog;
  • 타입 단언은 아무 상황에서나 쓰는 문법은 아님

    • 타입 단언의 규칙을 지켜야함

    • 단언식 : 값 as 단언 (A as B)

      • A가 B의 슈퍼 타입이거나
      • A가 B의 서브 타입 이어야함
        let num1 = 10 as never;
         // 10은 never의 슈퍼타입
         let num2 = 10 as unknown;
         // 10은 unknwon의 서브타입
      
         // let num3 = 10 as string 오류 발생
         let num3 = 10 as unknown as string;
         // 다중 선언 하면 되지만 권장x

📍 const 단언 (as const)

  • const 키워드를 써서 변수를 선언하는 것처럼 만들어줌

  • 객체 타입을 사용할 때 활용도가 높음

    • 모든 프로퍼티가 readonly (읽기 전용) 프로퍼티가 되는 객체로 추론됨 -> 프로퍼티의 값을 수정할 수 없는 객체가 됨
    • 프로퍼티가 굉장히 많은 객체를 초기화 할 때, 일일이 타입을 정의해서 readonly를 앞에 적을 필요가 없음
    let num4 = 10 as const;
    
    let cat = {
      name: '야옹이',
      color: 'yellow'
    } as const;
    
    // cat.name=''; 오류

📍 Non Null 단언

  • 어떤 값이 null이거나 undefined이 아니라고 타입스크립트 컴파일러에게 알려주는 역할을 함

    • 예를 들어서, 게시판을 만들때 게시글 객체 타입 정의를 할 때 익명으로 게시글을 남길 수도 있어서 타입에 선택적 프로퍼티를 추가 -> 변수를 만들어 초기화하고 게시글을 하나 만듦 -> author 이름의 길이를 몇 개인지 출력하는 기능을 만들어 보려 함

    • length 프로퍼티를 사용하면 자동으로 'post.author?.length'; 하고 ?(옵셔널 체이닝)가 추가되는 것을 볼 수 있음

      • author 프로퍼티가 없으면 값 전체를 undefined로 만들어주는 연산자
    • 그런데 값 전체가 undefined이 될 수 있어서, number 타입으로 정의한 len은 undefined가 될 수 없으므로 오류가 발생함

    • 이럴 때 'non null' 단언 사용 -> ! (author라는 프로퍼티는 있어! 하고 강조하는 느낌)

  • non null 단언 : 이 값이 null 이거나 undefined이 아닐 것이라고 타입스크립트 컴파일러가 믿도록 만듦

    • author 프로퍼티가 없는 상황이어도 타입 단언을 했기 때문에 타입스크립트는 이 값이 있을 것이라고 믿음
    type Post = {
      title: string;
      author?: string;
    };
    
    let post: Post = {
      title: '게시글1',
    };
    
    // const len: number = post.author?.length; 값 자체가 undefined가 될 수 있음 -> 오류
    const len: number = post.author!.length;

✨ 타입 좁히기

  • 조건문 등을 이용해 넓은 타입에서 좁은 타입으로 타입을 상황에 따라 좁히는 방법을 말함

  • 타입가드: 조건문과 함께 타입을 좁힐 수 있는 표현들

    • typeof 연산자 사용
    // value => number: toFixed
    // value => string: toUpperCase
    function func(value: number | string) {
      value; // string | number 타입으로 추론
      // value.toUpperCase(); 오류
      // value.toFixed(); 오류
    
      if (typeof value === 'number') {
        console.log(value.toFixed()); // number 타입으로 추론
      } else if (typeof value === 'string') {
        console.log(value.toUpperCase()); // string 타입으로 추론
      }
    }
    • 자바스크립트 연산자인 typeof 연산자는 null 값에 typeof를 적용해도 object로 반환함 -> Date 객체 뿐만 아니라 null 값도 obejct가 될 수 있다는 말
    function func(value: number | string | Date | null) {
    
      if (typeof value === 'number') {
        console.log(value.toFixed()); // number 타입으로 추론
      } else if (typeof value === 'string') {
        console.log(value.toUpperCase()); // string 타입으로 추론
      } else if (typeof value == 'object') {
        console.log(value.getTime()); // 오류 발생
        // value는 Date | null 값이 될 수 있는데 null은 getTime 메서드를 적용할 수 없음
      }
    }
    • 이럴 때는 instanceof 연산자 사용

      • instanceof : 왼쪽에 있는 값이 오른쪽 Class의 instance인지 물어보는 연산자
    • 그런데, 타입 별칭을 사용해 타입을 정의한 후, instanceof를 연산자를 적용하면 오류가 발생함

      • Date는 자바스크립트의 내장 클래스임 -> 따라서 오류가 발생하지 않지만 Person 이라는 타입 별칭으로 정의한 타입은 클래스가 아님
    function func(value: number | string | Date | null | Person) {
    
        if (typeof value === 'number') {
          console.log(value.toFixed()); // number 타입으로 추론
        } else if (typeof value === 'string') {
          console.log(value.toUpperCase()); // string 타입으로 추론
        } else if (value instanceof Date) {
          console.log(value.getTime()); // Date 타입으로 추론
        } else if (value instanceof Person) {
          console.log(`${value.nmae}${value.age}살 입니다.`); // 오류 발생
        }
      }
    • 이럴때는 in 연산자를 사용
      • 프로퍼티를 왼쪽에 써줘서 그 프로퍼티가 있는지 물어보도록 함
      • 그런데, in 연산자에는 null이나 undefined이 오면 안됨 but 아래의 코드에서는 null이 될 수 있으니깐 value가 있다는 조건을 하나더 적어주도록 했음 (value가 있을때에만 검사하도록 -> && 연산자 사용)
      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()); // number 타입으로 추론
        } else if (typeof value === 'string') {
          console.log(value.toUpperCase()); // string 타입으로 추론
        } else if (value instanceof Date) {
          console.log(value.getTime()); // Date 타입으로 추론
        } else if (value && 'age' in value) {
          console.log(`${value.nmae}${value.age}살 입니다.`); // Person 타입으로 추론
        }
      }

✨ 서로소 유니온 타입

  • 타입 좁히기를 할 때 더욱 직관적이게 타입을 좁힐 수 있도록 객체 타입을 정의하는 방법

  • 서로소 유니온 타입

    • 교집합이 없는 타입들로만 만든 유니온 타입을 말함
      e.g) string 타입과 number 타입의 집합 관계
  • 수학에서는 교집합이 하나도 없는, 즉 공통 원소가 하나도 없는 집합을 서로소 관계에 있다고함

  • 언제 사용하면 좋을까?

    • 예를 들어서, 웹서비스에서 간단한 회원 관리 기능을 만든다고 가정할 때, 회원의 분류가 3가지로 가정하고 계정에 따라 메시지를 다르게 출력한다고 하면,
    type Admin = {
      name: string;
      kickCount: number;
    };
    
    type Member = {
      name: string;
      point: number;
    };
    
    type Guest = {
      name: string;
      visitCount: number;
    };
    
    type User = Admin | Member | Guest;
    
    // Admin -> {name}님 현재까지 {kickCount}명 강퇴했습니다.
    // Member -> {name}님 현재까지 {point} 모았습니다.
    // Guest -> {name}님 현재까지 {visitCount}번 오셨습니다.
    function login(user: User) {
      if('kickCount' in user) {
        console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
      } else if ('point' in user) {
        console.log(`${user.name}님 현재까지 ${user.point} 모았습니다.`);
      } else {
        console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다.`);
      }
    }
    • 함수만 보고 admin, member, guest를 한눈에 파악하기 어려움. 결국 위의 타입 프로퍼티들을 봐야 알 수 있음

    • 이럴 때 사용하는게 서로소 유니온 타입!

    • 아래처럼 string literal 타입을 사용해서 admin, member, guest의 교집합이 없게 만듦 -> 서로소 집합의 관계를 갖게 됨

    type Admin = {
      tag: 'ADMIN',
      name: string;
      kickCount: number;
    };
    
    type Member = {
      tag: 'MEMBER',
      name: string;
      point: number;
    };
    
    type Guest = {
      tag: 'GUEST',
      name: string;
      visitCount: number;
    };
    
    type User = Admin | Member | Guest;
    
    function login(user: User) {
      switch (user.tag) {
        case 'ADMIN' : {
          console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
          break;
        }
        case 'MEMBER' : {
          console.log(`${user.name}님 현재까지 ${user.point} 모았습니다.`);
          break;
        }
        case 'GUEST' : {
          console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다.`);
          break;
        }
      }
    }
  • 하나의 예를 더 살펴보자.

    • 비동기 작업의 결과물을 처리하는 객체의 타입을 아래처럼 정의 했을 때, switch문에서 옵셔널 체이닝 연산자 또는 non null 연산자를 사용해야하는 것을 볼 수 있음 -> 안전한 코드는 아님
    /**
     * 복습 겸 한가지 더 사례
     */
    
    // 비동기 작업의 결과를 처리하는 객체
    
    type AsyncTask = {
      state: 'LOADING' | 'FAILED' | 'SUCCESS';
      error?: {
        message: string;
      };
      response?: {
        data: string;
      }
    };
    
    // 로딩 중 -> 콘솔에 로딩중 출력
    // 실패 -> 실패 : 에러 메시지 출력
    // 성공 -> 성공 : 데이터 출력
    function processResult (task: AsyncTask) {
      switch(task.state) {
        case 'LOADING': {
          console.log('로딩 중');
          break;
        }
        case 'FAILED': {
          console.log(`에러 발생: ${task.error?.message}`);
          break;
        }
        case 'SUCCESS': {
          console.log(`성공: ${task.response!.data}`);
          break;
        }
      }
    }
    
    const loading: AsyncTask = {
      state: 'LOADING',
    };
    
    const failed: AsyncTask = {
      state: 'FAILED',
      error: {
        message: '오류 발생 원인은 ~~',
      }
    };
    
    const success: AsyncTask = {
      state: 'SUCCESS',
      response: {
        data: '데이터~~',
      }
    };
    • 이럴 때, AsyncTask 타입을 세 개의 타입으로 분리해 서로소 유니온 타입으로 만들면 됨
    type LoadingTask = {
    state: 'LOADING';
    };
    type FailedTask = {
      state: 'FAILED';
      error: {
        message: string;
      };
    };
    type SuccessTask = {
      state: 'SUCCESS';
      response: {
        data: string;
      };
    };
    
    type AsyncTask = LoadingTask | FailedTask | SuccessTask;
    
    function processResult(task: AsyncTask) {
      switch (task.state) {
        case 'LOADING': {
          console.log('로딩 중');
          break;
        }
        case 'FAILED': {
          console.log(`에러 발생: ${task.error.message}`);
          break;
        }
        case 'SUCCESS': {
          console.log(`성공: ${task.response.data}`);
          break;
        }
      }
    }
    
    const loading: AsyncTask = {
      state: 'LOADING',
    };
    
    const failed: AsyncTask = {
      state: 'FAILED',
      error: {
        message: '오류 발생 원인은 ~~',
      },
    };
    
    const success: AsyncTask = {
      state: 'SUCCESS',
      response: {
        data: '데이터~~',
      },
    };
    • 동시에 여러 가지 상태를 표현 해야 하는 객체의 타입을 정의 할 때는 선택적 프로퍼티를 사용하는 것보단 상태에 따라서 타입들을 잘게 쪼개어서 state나 tag 같은 프로퍼티들을 추가해 서로소 유니온 타입으로 만들어 주는 것이 좋음! -> 더욱 직관적이고 안전한 코드를 만들 수 있게 도와줌
  • tag를 붙여서 객체들을 완벽히 구별해주어서 'Taged 유니온 타입'이라고도 함


출처: 한 입 크기로 잘라먹는 타입스크립트

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글