문법만 외워서는 제대로 쓸 수 없으니 타입스크립트의 원리부터 이해하자!
어떤 기준으로 타입을 정의하는지
어떤 기준으로 타입간의 관계를 정의하는지
어떤 기준으로 타입의 오류를 검사하는지
타입은 집합이다
타입
동일한 속성과 특징들을 갖는 여러값들을 모아둔 집합
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 |