
타입스크립트의 타입은 사실 여러개의 값을 포함하는 집합
집합은 동일한 속성을 갖는 여러개의 요소들을 하나의 그룹으로 묶은 단위number 타입하지만 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; // ❌
Number 로 값을 10으로 할당Number Literal인 10으로 값을 동일하게 10으로 할단이때 num1에 num2의 값을 저장하는건 가능
변수 num1의 타입(Number 타입)이 더 큰 타입이기 때문
Number 타입의 값을 Number Literal 타입의 변수에 할당하는 것
Number 타입의 값을 Number Literal 타입의 값으로 취급
더 큰 타입의 값(Number 타입)을 더 작은 타입(Number Literal 타입)의 값으로 취급하는 것은 불가

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

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 타입은 모든 타입의 서브타입이기 때문에 그 어떤 타입의 변수에도 다 값을 넣을 수 있음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 타입의 서브타입은 undefined 타입과 never 타입 밖에 없습니다. 따라서 void 타입에는 undefined, never 이외에 다른 타입의 값을 할당할 수 없습니다.
function voidExam() {
function noReturnFunc(): void {
console.log("hi");
return undefined;
}
let voidVar: void = undefined;
// void 타입은 undefined 타입의 슈퍼타입이다.
}
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 타입의 변수에는 어떤 타입의 변수로 다운캐스팅 절대불가
}
[ 🔽 타입 호환 표 ]

객체 타입간의 호환성
→ 어떤 객체타입을 다른 객체타입으로 취급해도 괜찮은가 ?
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 다운캐스팅
// 슈퍼타입
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",
});
대수 타입
→ 여러개의 타입을 합성해서 새롭게 만들어낸 타입
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: "",
};
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: "",
};
// 여기서 하나의 키 값이라도 빠지면 에러 발생. 모두 충족하여야 함.
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 num = 10;
// 10 = Number Literal 타입으로 추론
const str = "hello";
// "hello" = String Literal 타입으로 추론
// 공통 타입 추론
let arr = [1, "string"];
// (string | number)[] 타입으로 추론
type Person = {
name: string;
age: number;
};
// [1]
// let person: Person = {}; // ❌
// 여기서 객체를 넣지 않고
person.name = "";
person.age = 23;
// 이렇게 초기화 하고 싶을 때가 있다 하지만 이렇게 하면 에러 발생
// [2]
let person1 = {};
// person1.name = ""; // ❌
// person1.age = 23; // ❌
// 이번엔 person1 에 타입을 지정하지 않으면 에러 발생
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
값 as 타입 형식의 단언식을 A as B로 표현했을 때 아래의 두가지 조건중 한가지를 반드시 만족해야 함
let num1 = 10 as never; // ✅
// never 는 모든 타입의 서브타입
// A 가 B 의 슈퍼타입
let num2 = 10 as unknown; // ✅
// unknown 은 전체 집합
// A가 B의 서브타입이다
// let num3 = 10 as string; // ❌
let num33 = 10 as unknown as string;
// 이렇듯 중간에 값을 unknown 타입으로 단언하면 unknown 타입은 모든 타입의 슈퍼타입이므로 모든 타입으로 또 다시 단언하는게 가능합니다.
// 타입 단언은 실제로 그 값을 해당 타입의 값으로 바꾸는 것이 아니라 단순 눈속임에 불과 - 자주 사용하지 않음
특정 값을 const 타입으로 단언하면 마치 변수를 const로 선언한 것 과 비슷하게 타입이 변경
let num4 = 10 as const;
// 10 Number Literal 타입으로 단언됨
let cat = {
name: "야옹이",
color: "yellow",
} as const;
// 모든 프로퍼티가 readonly를 갖도록 단언됨
cat.name = ' ' // ❌
// . 으로 프로퍼티 접근 불가
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이 아닐것으로 단언
조건문 들을 이용해 넓은 타입에서 좁은 타입으로 상황에 따라 좁히는 방법
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}살 입니다`)
}
}
교집합이 없는 타입들로만 만든 유니온 타입
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;
// 유니온타입
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 를 달아줌
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}번 오셨습니다`);
}
}
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: "데이터 ~"
}
}