onebite TS - 3강. 타입스크립트 이해하기

박하늘·2025년 10월 1일

1️⃣ 타입은 집합이다

집합

타입스크립트의 타입은 사실 여러개의 값을 포함하는 집합

  • 집합은 동일한 속성을 갖는 여러개의 요소들을 하나의 그룹으로 묶은 단위
  • 여러개의 숫자 값들을 묶어 놓은 집합을 타입스크립트에서는 number 타입

Number Literal 타입은 다음 그림과 같이 딱 하나의 값만 포함하는 아주 작은 집합

  • 하지만 20 이라는 숫자는 Number Literal 타입임과 동시에 Number 타입이기도 함

  • 이처럼 타입스크립트의 모든 타입들은 집합으로써 서로 포함하고 또 포함되는 관계

  • 다른 타입을 포함하는 타입을 슈퍼 타입(부모 타입) / 반대는 서브 타입(자식 타입)


타입 호환성

타입 호환성이란 예를 들어 A와 B 두개의 타입이 존재할 때 A 타입의 값을 B 타입으로 취급해도 괜찮은지 판단하는 것을 의미

  • Number 타입과 Number Literal 타입이 있을 때 서브 타입Number Literal 타입의 값을 슈퍼 타입Number 타입의 값으로 취급하는 것은 가능

  • 그러나 반대로는 불가능 = 그 이유는 Number 타입이 더 큰 타입이기 때문

let num1: number = 10;
let num2: 10 = 10;

// 예제 1️⃣
num1 = num2;

// 예제 2️⃣
num2 = num1; // ❌
  • num1 에는 타입을 Number 로 값을 10으로 할당
  • num2 에는 타입을 Number Literal10으로 값을 동일하게 10으로 할단

예제 1️⃣

이때 num1에 num2의 값을 저장하는건 가능
변수 num1의 타입(Number 타입)이 더 큰 타입이기 때문

예제 2️⃣

Number 타입의 값을 Number Literal 타입의 변수에 할당하는 것
Number 타입의 값을 Number Literal 타입의 값으로 취급
더 큰 타입의 값(Number 타입)을 더 작은 타입(Number Literal 타입)의 값으로 취급하는 것은 불가

서브 타입의 값슈퍼 타입의 값으로 취급하는 것은 업 캐스팅 반대는 다운캐스팅
... 따라서 쉽게 정리하면 업캐스팅모든 상황에 가능하지만 다운 캐스팅대부분의 상황에 불가능하다고 할 수 있습니다.

2️⃣ 타입 계층도와 함께 기본타입 살펴보기

[ 🔽 기본 타입 계층도 ]

▪️ unknown

function unknownExam () {
    let a: unknown = 1;                 // number -> unknown
    let b: unknown = "hello";           // string -> unknown
    let c: unknown = true;              // boolean -> unknown
    let d: unknown = null;              // null -> unknown
    let e: unknown = undefined;         // undefined -> unknown
    let f: unknown = [];                // Array -> unknown
    let g: unknown = {};                // Object -> unknown
    let h: unknown = () => {};          // Function -> unknown
                                        // 이 list들은 업캐스팅 가능

    // ----------------------------

    let unknownValue: unknown;

    let num: number = unknownValue; // ❌
    let str: string = unknownValue; // ❌
    // unknown 타입은 number, string 타입 등에 할당할 수 없음 => 다운캐스팅 불가
}

▪️ Never

  • never 타입은 모든 타입의 서브타입이기 때문에 그 어떤 타입의 변수에도 다 값을 넣을 수 있음
  • never는 값이 존재할 수 없는 타입 => 반환 값이 없는 타입
function neverExam() {
    function neverFunc(): never {
        while (true) {}
    }

    let num: number = neverFunc();
    let str: string = neverFunc();
    // 전부 업캐스팅이기 때문

    // let never1: never = 10; // ❌
    // let never2: never = 'str' // ❌
    // never 타입이기 때문에 다운 캐스팅 즉, 어떤 값도 들어갈 수 없음
}
비유
• never = 빈 상자 (절대 채워질 수 없음)
• number = 숫자가 들어가는 상자

▶︎ 업캐스팅
  • “빈 상자”를 “숫자 상자”에 넣는 건 문제 없음.
  • 왜냐하면 어차피 빈 상자는 아무 값도 가지지 않으니까.

▶︎ 다운캐스팅
  • “숫자 상자(10)“를 “빈 상자”에 넣으려 하면 모순.
  • 빈 상자는 절대 값을 가질 수 없는데 10을 넣으려 하기 때문.

▪️ void

void 타입의 서브타입은 undefined 타입과 never 타입 밖에 없습니다. 따라서 void 타입에는 undefined, never 이외에 다른 타입의 값을 할당할 수 없습니다.

function voidExam() {
    function noReturnFunc(): void {
        console.log("hi");
        return undefined;
    }

    let voidVar: void = undefined;
    // void 타입은 undefined 타입의 슈퍼타입이다.
}

▪️ any

any 타입은 사실상 타입 계층도를 완전히 무시합니다. any는 일종의 치트키같은 타입입니다.
any는 뭐든지 예외입니다. 모든 타입의 슈퍼타입이 될 수도 있고 모든 타입의 서브 타입이 될 수도 있습니다. (never 제외)

function anyExam () {
    let anyValue: any;

    let num: number = anyValue;   // any -> number (다운 캐스트)
    let str: string = anyValue;   // any -> string (다운 캐스트)
    let bool: boolean = anyValue; // any -> boolean (다운 캐스트)

    let neverVar: never;

    anyValue = num;  // number -> any (업 캐스트)
    anyValue = str;  // string -> any (업 캐스트)
    anyValue = bool; // boolean -> any (업 캐스트)

    neverVar = anyValue; // ❌
    // never 타입의 경우 정말 순수한 공집합이기 때문에 never 타입의 변수에는 어떤 타입의 변수로 다운캐스팅 절대불가
}

[ 🔽 타입 호환 표 ]

3️⃣ 객체 타입의 호환성

객체 타입간의 호환성
→ 어떤 객체타입을 다른 객체타입으로 취급해도 괜찮은가 ?

▪️ 기본 예제

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; // ✅ OK 업캐스팅
dog = animal; // ❌ NO 다운캐스팅
  • Animal - 슈퍼타입 / Dog - 서브타입
  • Animal에 있는 키 값이 Dog에 모두 있기 때문에 Animal이 더 큰 범위에 속함

▪️ 추가 기본 예제

// 슈퍼타입
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 book: Book; 이렇게 선언한 변수에 book = programmingBook 이렇게 재선언이 되었지만
// 아래처럼 초기화 시 기존 Book 타입 이상의 프로퍼티가 들어가면 초과 프로퍼티 검사가 발동한다
let book2: Book = { // ❌
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
};

// ▪️ 초과 프로퍼티 - 매개변수에도 동일하게 발생
function func(book: Book) {}

func({ // 오류 발생
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
});

4️⃣ 대수타입

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

▪️ 합집합 - union 타입

let a: string | number | boolean;

a = 1;
a = "hello";
a = true;

// ✴︎ 배열 정의
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: "",
};

▪️ 교집합 타입 - Intersection 타입

let variable: number & string; 
// never 타입으로 추론된다 - 두 개의 타입이 공존하는 타입이 없기 때문

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

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

type Intersection = Dog1 & Person1;

let intersection1: Intersection = {
  name: "",
  color: "",
  language: "",
};
// 여기서 하나의 키 값이라도 빠지면 에러 발생. 모두 충족하여야 함.

5️⃣ 타입 추론

let a1 = 10;
// number 타입으로 추론

function func(param){} // ❌
// 매개변수는 타입을 설정해주지 않으면 암시적으로 any 타입으로 추론된다

▪️ 변수선언

let a = 10;
// number 타입으로 추론

let b = "hello";
// string 타입으로 추론

let c = {
  id: 1,
  name: "이정환",
  profile: {
    nickname: "winterlood",
  },
  urls: ["https://winterlood.com"],
};
// id, name, profile, urls 프로퍼티가 있는 객체 타입으로 추론

▪️ 구조 분해 할당

// ✴︎ 예제 1
let { id, name, profile } = c;

// 위 구조 분해 할당은 아래와 같은 의미
let id = c.id;           // 1
let name = c.name;       // "이정환"
let profile = c.profile; // { nickname: "winterlood" }

// ✴︎ 예제 2
let [one, two, three] = [1, "hello", true];

// 위 구조 분해 할당은 아래와 같은 의미
// one → 첫 번째 요소 1
// two → 두 번째 요소 "hello"
// three → 세 번째 요소 true

▪️ 함수의 반환값

함수는 반환값을 기준으로 타입 추론

function func() {
  return "hello";
}
// 반환값이 string 타입으로 추론된다

▪️ 기본값이 설정된 매개변수

function func1(message = "hello") {
  return "hello";
}

// message = string 으로 추론

▪️ 암시적 타입 추론

let d;
// 암시적인 any 타입으로 추론

d = 10; // 여기서 number 으로 타입 추론
d.toFixed();

d = "hello"; // any 타입의 진화로 string 으로 바뀜
d.toUpperCase();
d.toFixed(); // ❌ 오류 - number 타입만 가능한 메서드

▪️ const 상수의 추론

const num = 10;
// 10 = Number Literal 타입으로 추론

const str = "hello";
// "hello" = String Literal 타입으로 추론

// 공통 타입 추론
let arr = [1, "string"];
// (string | number)[] 타입으로 추론

6️⃣ 타입 단언

type Person = {
  name: string;
  age: number;
};
  • Person 이라는 타입에 각 키값에 대한 타입 선언

▪️ 1-1) 에러 예제

// [1]
// let person: Person = {}; // ❌
// 여기서 객체를 넣지 않고

person.name = "";
person.age = 23;
// 이렇게 초기화 하고 싶을 때가 있다 하지만 이렇게 하면 에러 발생

// [2]
let person1 = {};

// person1.name = ""; // ❌
// person1.age = 23; // ❌
// 이번엔 person1 에 타입을 지정하지 않으면 에러 발생

▪️ 1-2) 에러 예제 해결

as 로 타입 단언 !

let person = {} as Person;
// Person 타입으로 간주하고 봐줘 ~ 하고 단언 (as)

person.name = "";
person.age = 23; 

▪️ 타입 단언 시 초과 프로퍼티에 대한 ...

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

let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",
} as Dog
  • 위 코드에서는 breed 라는 초과 프로퍼티가 존재하지만 이 값을 Dog 타입으로 단언하여 초과 프로퍼티 검사 피함.

▪️ 2) 타입 단언의 규칙

값 as 타입 형식의 단언식을 A as B로 표현했을 때 아래의 두가지 조건중 한가지를 반드시 만족해야 함

  • A가 B의 슈퍼타입이다
  • A가 B의 서브타입이다
let num1 = 10 as never;   // ✅
// never 는 모든 타입의 서브타입
// A 가 B 의 슈퍼타입

let num2 = 10 as unknown; // ✅
// unknown 은 전체 집합
// A가 B의 서브타입이다

// let num3 = 10 as string;  // ❌

▪️ 3) 다중 단언

let num33 = 10 as unknown as string;
// 이렇듯 중간에 값을 unknown 타입으로 단언하면 unknown 타입은 모든 타입의 슈퍼타입이므로 모든 타입으로 또 다시 단언하는게 가능합니다.
// 타입 단언은 실제로 그 값을 해당 타입의 값으로 바꾸는 것이 아니라 단순 눈속임에 불과 - 자주 사용하지 않음

▪️ 4) const 선언

특정 값을 const 타입으로 단언하면 마치 변수를 const로 선언한 것 과 비슷하게 타입이 변경

let num4 = 10 as const;
// 10 Number Literal 타입으로 단언됨

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;
// 모든 프로퍼티가 readonly를 갖도록 단언됨

cat.name = ' ' // ❌
// . 으로 프로퍼티 접근 불가

▪️ 5) non null(!) 단언


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

let post: Post = {
  title: "게시글1",
};

const len: number = post.author?.length; // ❌
// ? 옵셔널 체이닝 은 값이 없을 시 undefined
// 원래는 자동으로 이렇게 ? 옵셔널 체이닝이 들어가지만, number로 타입 단언 시 undefined 가 들어갈 수 없음으로 에러 발생

const len1: number = post.author!.length;
// ? 를 ! 로 바꿔주면 이 값이 undefined이거나 null이 아닐것으로 단언

7️⃣ 타입 좁히기 ( typeof )

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

  • ex) typeof value === "number" - typeof타입 가드 라고 함
function func(value: number | string) {
  value.toFixed() // ❌ 오류 - number | string 으로 추론
  value.toUpperCase() // ❌ 오류 -  number | string 으로 추론

  // 타입 좁히기 - 조건문으로 각 타입으로 좁히는 것
  if (typeof value === "number") {
    console.log(value.toFixed());
    // 여기 value 는 number 타입으로 추론
    
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
    // 여기 value 는 string 타입으로 추론
  }

}

▪️ 추가 예제

function func1(value: number | string | Date | null) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (value instanceof Date) {
    // value 가 Date 객체냐? 라는 뜻
    console.log(value.getTime());
  }
}

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

function func2(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 (value instanceof Date) {
    console.log(value.getTime());
  } else if (value && "age" in value) {
    // 여기는 value instanceof Person 불가능
    // 오른쪽에는 반드시 클래스(생성자 함수 - 즉 타입 불가) 가 와야 합니다.
    // Person은 함수나 클래스가 아니라 타입이라서 오류
    // value 가 null 일 수 있기 때문에 있을 경우 라는 조건 걸어주기
    // in 을 사용하여 우리가 직접 넣은 프로퍼티를 사용할 수 있도록
    console.log(`${value.name}${value.age}살 입니다`)
  }
}

8️⃣ 서로소 유니온 타입

교집합이 없는 타입들로만 만든 유니온 타입

  • ex) number string 타입

▪️ 1) 타입 선언

import { error } from "console";

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;
// 유니온타입

▪️ 1-1) tag 달기 전

function login(user: User) {
  if ("kickCount" in user) {
		// Admin
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
  } else if ("point" in user) {
		// Member
    console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
  } else {
		// Guest
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
  }
}
// 이 login 함수를 보고 한 번에 어떤 회원들인지 확인하기 어렵다 직관적이지 않다
// 이럴 때 각 타입에 tag 를 달아줌

▪️ 1-2) tag 작성 후

function logiTag(user: User) {
  if (user.tag === "ADMIN") {
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
  } else if (user.tag === "MEMBER") {
    console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
  } else {
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
  }
}

▪️ 1-3) case 사용

function loginCase(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;
    }
  }
}

▪️ 추가 예제


type LoadingTask = {
    state: "LOADING"
}

type FailedTask = {
    state: "FAILED"
    error?: {
        message: string
    }
}

type SuccessTast ={
    state:"SUCCESS"
    response?: {
        data: string
    }
}

type AsyncTask = LoadingTask | FailedTask | SuccessTast

// 이렇게 타입을 지정 해주면 processResult 함수에서 task.error?.message 이렇게 접근된다
// type AsyncTask = {
//     state: "LOADING" | "FAILED"| "SUCCESS",
//     // state: string, 보다는 위처럼 직관적이게
//     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: "데이터 ~"
    }
}
* 이 글의 모든 사진의 출처는 [ 인프런 강의 - one bite typescript ]

0개의 댓글