공부/TIL

한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 이해하기

2023. 8. 18. 18:01
목차
  1. 타입은 집합이다
  2. 타입 호환성
  3. 객체 타입 호환성
  4. 초과 프로퍼티 검사
  5. 대수 타입
  6. 타입 추론
  7. 타입 단언
  8. 타입 좁히기
  9. 서로소 유니온 타입
  10. 복습 : 비동기 작업의 결과를 처리해보자!
문법만 외워서는 제대로 쓸 수 없으니 타입스크립트의 원리부터 이해하자!

 

어떤 기준으로 타입을 정의하는지
어떤 기준으로 타입간의 관계를 정의하는지
어떤 기준으로 타입의 오류를 검사하는지

 

타입은 집합이다

타입

동일한 속성과 특징들을 갖는 여러값들을 모아둔 집합

 

number Literal Type

= number Type의 부분집합

- nuber Literal Type : 서브타입, 자식타입

- number Type : 슈퍼타입, 부모타입

 

타입 호환성

어떤 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것

number Literal Type -> number Type : 호환됨(ex. 정사각형 -> 직사각형)

number Literal Type <- number Type : 호환 안됨(ex. 정사각형 <- 직사각형)

 

업캐스팅

서브타입을 슈퍼타입으로 취급하는 것

모든 상황에 허용됨

let num: 1 number = 10;
let num2: 10 = 10;

num1 = num2; // 됨

 

다운캐스팅

슈퍼타입을 서브타입으로 취급하는 것

대부분의 상황에 허용되지 않음

let num: 1 number = 10;
let num2: 10 = 10;

num2 = num1; // 안됨

 

타입계층도

 

Unknown Type

모든 타입들의 슈퍼 타입(전체 집합)

// 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
}

 

Never Type

모든 타입의 서브집합(공집합)

// 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;
}

 

Void Type

undefined의 슈퍼타입

// Void 타입
// undefined의 슈퍼타입

function voidExam() {
    function voidFunc(): void {
        console.log("hi");
    }

    let voidVar: void = undefined;
}

 

Any Type

타입 계층도를 무시함

치트키 : 모든 타입의 슈퍼타입이자 서브타입이기도 함. never 타입만 제외하고!

// any 타입
// 타입계층도를 무시함
// 치트키(모든 타입의 슈퍼타입이자 서브타입이기도 함.never 타입만 제외하고!)

function anyExam() {
    let unknownVar: unknown;
    let anyVar: any;
    let undefinedVar: undefined;
    let neverVar: never;

    anyVar = unknownVar; // 가능!

    undefinedVar = anyVar; // 가능!

    neverVar = anyVar; // 오류!
}

 

객체 타입 호환성

어떤 객체 타입을 다른 객체 타입으로 취급해도 괜찮은가?

property 기준으로 누가 슈퍼고 서브인지 판단 가능

// 객체 타입간의 호환성

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

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

let animal: Animal = {
    name: "기린",
    color: "yellow",
};

let dog: Dog = {
    name: "돌돌이",
    color: "brown",
    breed: "진도",
};

animal = dog; // 허용
dog = animal; // 오류

구조적 타입 시스템에 따라,

- name, color를 가졌으면 Animal에 속함

- name, color, breed까지 가져야 Dog에 속함

-> Animal > Dog

 

초과 프로퍼티 검사

객체 타입 변수를 초기화할때 객체 literal을 사용하면, 초과 프로퍼티, 즉 실제 타입에는 정의해놓지 않은 프로퍼티 작성 시 허용되지 않도록 하는 검사

type Book = {
    name: string;
    price: number;
};

type ProgrammingBook = {
    name: string;
    price: number;
    skill: string;
};

let book: Book;
let programmingBook: ProgrammingBook = {
    name: '한 입 크기로 잘라먹는 리액트',
    price: 33000,
    skill: 'reactjs'
}

book = programmingBook; // 허용
// programmingBook = book; // 오류

let book2: Book = {
    name: '한 입 크기로 잘라먹는 리액트',
    price: 33000,
    skill: 'reactjs' // 오류!!!
}

book = programmingBook; 은 되고, book2는 안되는 이유

-> 전자는 객체 literal을 사용하지 않아 검사가 발동하지 않고 후자는 사용하였기 때문에 검사가 발동함

 

대수 타입

여러 개의 타입을 합성해서 새롭게 만들어낸 타입

 

합집합 타입 = Union Type

// 합집합 = Union 타입
let a: string | number; // string number union type이라고 부르면 됨
a = 1;
a = "hello";

// let a: string | number | boolean | undefined... 개수 제한 없이 다양하게 가능

let arr: (number | string | boolean)[] = [1, "hello", true];

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

type Person = {
    name: string;
    language: string;
}

type Union1 = Dog | Person

let union1: Union1 = {
    name: "",
    color: ""
};

let union2: Union1 = {
    name: "",
    language: ""
}

let union3: Union1 = { // 교집합
    name: "",
    color: "",
    language: ""
}

let union4: Union1 = { // 오류 발생!
    name: ""
}

union4에서 오류가 발생하는 이유 : Dog, Person 아무데도 포함되지 않음 = 합집합의 밖에 있음

 

교집합 타입 = Intersection Type

// 교집합 타입 = Intersection Type

let variable: number & string; // 서로 공유하거나 겹치는 값이 없음 = never type으로 취급됨

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

type Person = {
    name: string;
    language: string;
}

type Intersection = Dog & Person; // 두 type의 property를 모두 가져야 함

let intersection11: Intersection = {
    name: "",
    color: "",
    language: ""
}

 

타입 추론

타입을 정의하지 않아도 할당된 초기값에 따라 자동으로 타입을 추론함

함수의 경우 return 값, 혹은 기본 값에 따라 추론함

단, 타입을 추론하지 못하는 상황 등이 존재하니 유의해야 함

 

타입 넓히기

let으로 변수 선언 시 범용적으로 타입을 정의함

// 타입 추론

let a = 10;
let b = "hello";
let c = {
    id: 1,
    name: "이정환",
    profile: {
        nickname: "winterlood",
    },
    urls: ["https://winterlood.com"],
};

let {id, name, profile} = c;

let [one, two, three] = [1, "hello", true];

function func() {
    return "hello"; // 자동 추론
}

function func2 (msg = "hello") { // 자동 추론
    return msg
}

 

any type 진화

암묵적 any type : 초기값 등 아무런 정보가 없으니 암묵적으로 any로 선언함

값을 선언하는 순간 해당 타입으로 그때마다 진화함

let d; // any type으로 추론됨
d = 10;
d // number type으로 진화

d = "hello"
d // string type으로 진화

 

const 선언

const로 선언 시 값이 바뀌지 않기 때문에 literal로 선언됨

const num = 10; // const num: 10
const str = "hello"; // const str: "hello"

 

배열 추론

let arr = [1, "string"] // let arr: (string | number)[]

 

타입 단언

의도와 다르게 타입이 추론되어 원하는 기능을 만들기 어려울 때 사용

단언식 : 값 as 단언(타입)

// 타입 단언

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: "진도" // 값을 추가하고 싶지만 단언하지 않으면 오류남
} as Dog; // 단언해주면 오류 안남

 

타입 단언의 규칙

단언식에서 A as B 일때, A가 B의 슈퍼타입이거나 A가 B의 서브타입이어야 함

let num1 = 10 as never; // never가 number의 서브타입이기 때문에 타입 단언이 가능함
let num2 = 10 as unknown; // unknown이 number의 슈퍼타입
let num3 = 10 as string; // 오류 발생 : number와 string은 겹치는 부분이 없음

 

다중 단언

단언이 불가능한 상황에서도 단언이 되도록 할 수 있음 : 가능하면 사용하지 않을 것을 권장함

let num3 = 10 as unknown as string; // number이 unknown의 서브타입이고, unknown이 string의 슈퍼타입이기 때문에 타입 단언 가능

 

const 단언

const 선언과 동일한 효과를 만들어주는 단언

객체 타입일 때 편리하게 사용 가능

let num4 = 10 as const;

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

cat.name = ""; // 오류 발생 : const로 선언하여 readOnly로 추론됨 => 값 수정 안됨

Non Null 단언

해당 값이 null이나 undefined이 아님을 ts 컴파일러에게 알림

!를 추가하면 됨

왠만하면 사용 비추천(버그의 가능성이 있음)

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

let post : Post = {
    title: "게시글",
    author: "이정환"
};

const len : number = post.author!.length; // ! : author은 null이나 undfined가 아니다!

 

타입 좁히기

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

// value = number => toFixed
// value = string => toUpperCase
function func(value : number | string) {
    value.toUpperCase(); // value의 타입이 number | string이기 때문에 에러 발생
    value.toFixed(); // 동일한 이유로 에러 발생
    if (typeof value === 'number') {
        console.log(value.toFixed()) // 여가선 value의 타입은 number
    }
    else if (typeof value === 'string') {
        console.log(value.toUpperCase()) // 여기선 value의 타입은 string
    }
}

타입가드 Type Guard

이때 if (typeof value === 'number')처럼 타입을 좁히는 표현들을 Type Guard라고 부름

다른 타입가드 표현

instanceof 사용

    else if (value instanceof Date) { // 왼쪽의 값이 오른쪽의 instance인지
        console.log(value.getTime())
    }

in 사용

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

// 생략

else if (value && 'age' in value) { // null 값 처리를 위해 value &&으로 'value 존재 시'라는 조건 추가
        console.log(`${value.name}은 ${value.age}살 입니다`)
    }

 

서로소 유니온 타입

교집합이 없는 타입(서로소 관계*)들로만 만든 유니온 타입

ex. string | number

*서로소 관계 : string type, number type처럼 교집합이 없는 관계

해당 코드의 문제점

각 조건문에서 무슨 타입을 취급하는지 한눈에 보이지 않는다.

 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(`${name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`)
    } else if ('point' in user) {
        console.log(`${name}님 현재까지 ${user.point} 모았습니다.`)
    } else {
        console.log(`${name}님 현재까지 ${user.visitCount}번 오셨습니다.`)
    }
 } // 직관적으로 각 조건문에서 무슨 타입을 취급하는지 알기 힘들다

해결법

1. Admin, Member, Guest 타입에 각각 tag를 추가하여 if문 사용 시 좀더 직관적으로 만들 수 있다.

 type Admin = {
    tag: 'ADMIN'; // tag를 추가
    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;

 // Admin => {name}님 현재까지 {kickCount}명 강퇴했습니다.
 // Member => {name}님 현재까지 {point} 모았습니다.
 // Guest => {name}님 현재까지 {visitCount}번 오셨습니다.
 function login(user: User) {
    if (user.tag === 'ADMIN') {
        console.log(`${name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`)
    } else if (user.tag === 'MEMBER') {
        console.log(`${name}님 현재까지 ${user.point} 모았습니다.`)
    } else {
        console.log(`${name}님 현재까지 ${user.visitCount}번 오셨습니다.`)
    }
 }

2. switch문을 이용하여 if문보다 좀더 직관적으로 볼 수 있다.

 function login(user: User) {
    
    switch (user.tag) {
        case 'ADMIN': {
            console.log(`${name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`)
            break;
        }
        case 'MEMBER': {
            console.log(`${name}님 현재까지 ${user.point} 모았습니다.`)
            break;
        }
        case 'GUEST': {
            console.log(`${name}님 현재까지 ${user.visitCount}번 오셨습니다.`)
            break;
        }
    }
 }

=> 기존에는 Admin, Member, Guest가 겹칠 수 있었는데(name, kickCount, point, visitCount 동시에 가진 객체 존재 가능) string literal 타입인 tag가 ADMIN이자 MEMBER 일 순 없기 때문에 서로소 관계가 된다. 따라서 switch 문으로 해당 타입으로만 좁힐 수 있다.

 

복습 : 비동기 작업의 결과를 처리해보자!

해당 코드의 문제점 : error나 response가 선택적 property기 때문에 switch문에서 원하는 기능으로 사용할 수 없음

 // 복습 : 비동기 작업의 결과를 처리하는 객체

type AsyncTask = {
    // state: string; // 가능은 하나 구체적인 편이 좋음
    state: 'LOADING' | 'FAILED' | 'SUCCESS';
    error?: { // state가 loading일 땐 없어야함
        message: string;
    };
    response?: { // state가 loading일 땐 없어야함
        data: string;
    };
};

// 로딩 중 : 콘솔에 로딩 중 출력
// 실패 : 에러 메세지 출력
// 성공 : 데이터 출력
function processResult(task: AsyncTask) {
    switch (task.state) {
        case 'LOADING': {
            console.log('로딩 중');
            break;
        }
        case 'FAILED': {
            console.log(`에러 발생 : ${task.error.message}`) // task 타입이 FAILED로 정의 안됨. error는 선택적 property기 때문
            console.log(`에러 발생 : ${task.error?.message}`) // ?(optional chaining)나 !(not null)을 통해 오류가 안나도록 할 순 있으나 안전하지 않음
            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: '데이터 ~',
    },
 };

해결법 : 선택적 프로퍼티 대신 LoadingTask, FailedTask, SuccessTask로 타입을 쪼갠다.

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}`) // task 타입이 FAILED로 정의 안됨. error는 선택적 property기 때문
            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: '데이터 ~',
    },
 };

 

'공부 > TIL' 카테고리의 다른 글

[혼자 공부하는 컴퓨터구조+운영체제] Chapter 10 - 11  (0) 2023.09.09
[혼자 공부하는 컴퓨터구조+운영체제] Chapter 8 - 9  (0) 2023.09.02
한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 기본  (0) 2023.08.14
한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 개론  (1) 2023.08.09
[혼자 공부하는 컴퓨터구조+운영체제] Chapter 6 - 7  (0) 2023.08.05
  1. 타입은 집합이다
  2. 타입 호환성
  3. 객체 타입 호환성
  4. 초과 프로퍼티 검사
  5. 대수 타입
  6. 타입 추론
  7. 타입 단언
  8. 타입 좁히기
  9. 서로소 유니온 타입
  10. 복습 : 비동기 작업의 결과를 처리해보자!
'공부/TIL' 카테고리의 다른 글
  • [혼자 공부하는 컴퓨터구조+운영체제] Chapter 10 - 11
  • [혼자 공부하는 컴퓨터구조+운영체제] Chapter 8 - 9
  • 한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 기본
  • 한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 개론
Ail_
Ail_
Ail_
log
Ail_
  • 분류 전체보기 (181)
    • 사설 (11)
      • 강연 (5)
      • * (3)
      • 회고 (3)
    • 공부 (161)
      • Just do it (3)
      • TIL (66)
      • [TIL] Game Bootcamp (31)
      • [Project] Game Bootcamp (1)
      • [TIL] Digital Twin Bootcamp (39)
      • [Project] Digital Twin Boot.. (21)
    • 노션 (3)

인기 글

최근 글

태그

  • 데이터베이스
  • 멋쟁이사자처럼
  • 이펙트
  • node.js
  • 개발회고
  • 피격
  • 오블완
  • 노션
  • 개발일지
  • 유니티 게임 개발
  • SQL첫걸음
  • 플레이어
  • notion
  • unity
  • 인터랙티브 웹 UX·UI
  • Chat
  • 대시
  • 한입 크기로 잘라 먹는 타입스크립트
  • 공격
  • 티스토리챌린지
  • 회고
  • C#
  • Do it! 자료구조와 함께 배우는 알고리즘 입문 : 파이썬 편
  • mysql 설치
  • 배포
  • 템플릿
  • 부트캠프
  • TypeScript
  • 유니티
  • SQL 첫걸음

최근 댓글

전체
오늘
어제
hELLO · Designed By 정상우.
Ail_
한입 크기로 잘라 먹는 타입스크립트 : 타입스크립트 이해하기
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.