티스토리 뷰
1. 타입과 집합
타입스크립트의 타입은 동일한 속성과 특징을 갖는 여러 값들을 모아둔 집합을 의미한다.
타입 사이의 관계
부모(수퍼) - 자식(서브) 관계
어떤 타입 A가 다른 타입 B의 부분집합이라면, 이때 A를 자식타입, B를 부모타입이라고 한다.
EX. number literal type은 number type의 부분집합이므로
number literal type은 자식타입, number type은 부모타입
이 관계를 계층처럼 표시하면 다음과 같다.
다음은 타입스크립트에서 제공하는 전체적인 타입 계층도이다.
타입 호환성
어떤 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것을 말한다.
예시
- number 타입을 number literal 타입으로 취급 불가능
- number literal 타입을 number 타입으로 취급 가능
let num1:number = 10; // number
let num2:10 = 10; // number literal
num1 = num2; // 가능
num2 = num1; // 불가능
즉, 더 작은 타입의 변수에 더 큰 타입의 값을 할당할 수 없다. 타입스크립트에서는 수퍼타입의 값을 서브타입의 값으로 취급하는 것을 허용하지 않는다.
업 캐스팅과 다운 캐스팅
- 업 캐스팅 : 서브/부모 -> 수퍼/자식 취급 (모든 상황에 허용 O)
- 다운 캐스팅 : 수퍼 -> 서브 취급 (대부분의 상황에 허용 X)
2. 타입 계층도 살펴보기
(1) unknown 타입
모든 타입의 수퍼타입. 전체 집합이라고도 한다.
모든 타입에 업캐스팅이 가능하다. 즉, unknown 타입의 변수에는 모든 타입의 값을 집어넣을 수 있다.
function unknownExam() {
// 업캐스팅
let a: unknown = 1;
let b: unknown = "hello";
let c: unknown = true;
let d: unknown = null;
let e: unknown = undefined;
// 다운캐스팅 불가능
let unknownVa: unknown;
// let num: number = unknownVa; // 에러
// let bool: boolean = unknownVa; // 에러
}
반대로 unknown 타입의 값은 any를 제외한 어떤 타입의 변수에도 할당할 수 없다(다운캐스팅 불가능).
(2) never 타입
never 타입은 모든 타입의 서브타입이다. 즉, 모든 타입의 부분집합이자 공집합이라고 할 수 있다.
모든 타입의 변수에 never 타입의 변수를 할당할 수 있다.
반대로 never 타입의 변수에는 어떤 값도 저장되어선 안되고 아무런 값도 저장할 수 없다.
function neverExam() {
function neverFunc(): never {
while (true) {}
}
// 업캐스팅
let num: number = neverFunc();
let str: string = neverFunc();
// 다운캐스팅 불가능
// let n1: never = 10; // 에러
}
(3) void 타입
아무것도 반환하지 않은 함수에 정의하는 void 타입은 undefined 타입의 수퍼타입이다. 따라서 undefined 타입은 void 타입이라고 말할 수 있기에 아래와 같이 void 타입의 함수에 리턴문을 적어주는 것도 가능하다.
function voidExam() {
function voidFunc(): void {
console.log("hi");
return undefined; // 업캐스팅
}
// 업캐스팅
let voidVar: void = undefined;
}
(4) any 타입
any 타입은 모든 타입의 수퍼타입이 될수도 있고, 모든 타입의 서브타입이 될 수도 있다(never 제외).
타입계층도를 완전히 무시하는 타입이라고 볼 수 있다.
function anyExam() {
let unknownVar: unknown;
let anyVar: any;
let undefinedVar: undefined;
let neverVar: never;
// 모든 타입의 수퍼타입 => 다운 캐스팅 가능
anyVar = unknownVar;
undefinedVar = anyVar;
// 한가지 예외 : never 타입에 다운캐스팅은 불가능
// neverVar = anyVar; // 불가능
}
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; // 가능
dog = animal; // 불가능
Animal, Dog라는 두 타입을 정의하고 Animal에는 name과 color 프로퍼티를, Dog에는 name, color, breed 프로퍼티를 정의해준다.
이후 각 타입별 변수를 만들고 서로를 서로에게 할당했을 때,
animal = dog는 가능하지만, dog = animal은 불가능하다. 즉, dog 객체는 animal 객체가 될 수 있지만, animal 객체는 dog 객체가 될 수 없다(프로퍼티 부족).
이를 통해 animal이 수퍼타입, dog가 서브 타입인 것을 알 수 있다.(프로퍼티가 더 작은 게 수퍼타입이 된다.)
초과 프로퍼티 검사
두 번째 예시를 보자.
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;
첫 번째 예시와 유사하게 수퍼타입인 Book과 서브타입인 ProgrammingBook 타입을 정의하고 book 변수에 programmingBook 객체를 할당하였다.
이와 똑같은 의도로 동작을 하는 아래의 코드를 보자.
let book2: Book = {
name: "한 입 크기로 잘라먹는 리액트",
price: 33000,
// skill: "reactjs", // 오류
};
위의 코드처럼 Book 타입 변수를 처음부터 programmingBook 객체로 초기화하면 오류가 발생한다.
이는 초과 프로퍼티 검사 때문인데,
변수를 초기화하거나 매개변수로 전달할 때 "객체 리터럴"을 사용하면 검사하는 과정을 거치게 된다. 이 검사에서 실제 타입에 정의되지 않은 프로퍼티를 정의하지 않도록 막는다.
따라서 위의 코드에서는 오류를 발생하게 되는 것이다.
반대로 초기화 후 서브 객체 변수를 할당하면 초과 프로퍼티 검사과정을 거치지 않는다.
4. 대수 타입
여러 개의 타입을 합성해서 새롭게 만들어낸 타입을 의미한다.
합집합 타입과 교집합 타입이 존재한다.
(1) 합집합 Union 타입
아래의 예시를 참고하면 쉽게 이해할 수 있다. 참고로 |
를 이용해 추가할 수 있는 타입의 개수는 무한대이다.
let a: string | number | boolean;
a = 10;
a = "hello";
a = true;
let arr: (number | string | boolean)[] = [true, 10, "hello"];
또한, 타입 별칭을 이용해서 유니온 타입을 만드는 것도 가능하다.
type Dog = {
name: string;
color: string;
};
type Person = {
name: string;
language: string;
};
// 타입 별칭을 이용한 유니온 타입
type Union1 = Dog | Person;
// Dog에 해당하는 객체
let union1: Union1 = {
name: "",
color: "",
};
// Person에 해당하는 객체
let union2: Union1 = {
name: "",
language: "",
};
// Dog + Person 합친 프로퍼티로 초기화도 가능하다.
// 이런 객체는 Dog 객체이면서도 Person 객체이기도 한,
// 교집합에 있는 객체가 된다.
let union3: Union1 = {
name: "",
color: "",
language: "",
};
(2) 교집합 Intersection 타입
기본 타입(number ,string, boolean 등등)으로 교집합 타입을 정의하면 대부분 never 타입이 된다.
따라서 교집합 타입은 객체 타입에 많이 사용된다.
let variable: number & string; // never ; 불가능한 타입 ; 공집합을 의미
// Dog와 Person 예시
// Dog 타입이면서 동시에 Person 타입인 객체
type Intersection = Dog & Person;
let inter1: Intersection = {
name: "",
color: "",
language: "",
};
5. 타입 추론
타입스크립트에는 변수에 타입이 정의되어 있지 않아도 초기값을 넣어주면 자동으로 변수의 타입을 추론해주는 시스템이 있다.
let으로 선언한 변수는 조금 더 범용적인 타입으로 자동 추론한다(타입 넒히기).
// 자동 추론의 기준은 초깃갑
let a = 10;
let b = "hello";
let c = {
id: 1,
name: "홍길동",
profile: {
nickname: "hong",
},
url: ["https://"],
};
// 구조 분해 할당도 가능
let { id, name, profile } = c;
let [one, two, three] = [1, "hello", true];
function func(message = "hello") {
return "hello";
}
초기값이 생략되면 암묵적인 any 타입으로 추론된다. 암묵적 any타입으로 추론되면 타입이 계속 진화할 수 있다.
let d;
d = 10; // number로 추론됨
d.toFixed();
d = "hello"; // string으로 추론됨
d.toUpperCase();
const로 지정되면 literal 타입으로 추론된다.
const num = 10; // number literal 타입으로 추론
const str = "hello"; // string literal
6. 타입 단언
초기화 값의 타입을 지정해주는 방법이다.
초기화 값 뒤에 as + 타입명
을 붙여줘서 초기화 값의 타입을 직접 단언한다.
타입 단언의 규칙은 다음과 같다.
1. 값 as 단언
,A as B
형태에서
2. A가 B의 수퍼타입이거나
3. A가 B의 서브타입이어야 한다.
type Person = {
name: string;
age: number;
};
let person = {} as Person; // 타입 단언
person.name = "홍길동";
person.age = 1999;
초기화 시 초과 프로퍼티 검사로 인한 에러를 막기 위해 타입 단언을 활용할 수도 있다. 이때 변수 뒤에 붙여뒀던 타입 주석을 제거해도 제대로 타입을 추론해준다.
type Dog = {
name: string;
color: string;
};
let dog = {
name: "돌돌이",
color: "brown",
breed: "진도",
} as Dog;
const 단언
변수를 const로 선언한 것과 동일한 효과를 만들어주는 단언을 말한다.
let num4 = 10 as const; // number literal type
let cat = {
name: "야옹",
color: "yellow",
} as const; // 객체의 모든 프로퍼티가 readonly 읽기 전용 프로퍼티로 추론됨
Non Null 단언
어느 값이 null 혹은 undefined가 아니라고 알려주는 단언을 말한다.
non null !
연산자를 활용하여 컴파일러에게 알려준다.
// author 프로퍼티는 존재할 수도, 존재하지 않을 수도 있다면?
type Post = {
title: string;
author?: string; // 선택적 프로퍼티
};
let post: Post = {
title: "post1",
author: "홍길동",
};
// ! : Non Null 연산자 => null, undefined가 아닐 것
const len: number = post.author!.length;
7. 타입 좁히기
조건문 등을 이용해 넓은 타입에서 좁은 타입으로 타입을 상황에 따라 좁히는 방법을 말한다.
이때 타입 좁히기를 도와주는 도구들을 타입 가드라고 하는데, 그 종류는 아래와 같다.
- typeof
- A instanceof B : A가 B 클래스의 인스턴스 객체인지 검사
- in : 클래스 객체가 아닌 특정한 타입 객체인지 검사하고 싶다면 프로퍼티 검사
// 매개변수가 number면 toFixed
// string이면 toUpperCase 적용
// Date면 getTime
// Person이면 age 출력
type Person = {
name: string;
age: number;
};
function f(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) {
console.log(value.age);
}
}
8. 서로소 유니온 타입
교집합이 없는 타입들로만 만든 유니온 타입을 말한다.
- ex. number과 string 타입
서로소 유니온 타입을 활용하면 더욱 편리한 타입 좁히기가 가능하다.
아래의 예시는 회원 타입별 로그인 기능을 구현한 것이다. Admin, Member, Guest 타입에 tag 프로퍼티가 string literal로 정의되어 있어서 세 타입은 모두 서로소 집합이 된다. 즉, 세 타입을 모두 만족하는 객체를 만들어낼 수 없다.
// 회원의 분류
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; // 서로소 union type이 된 User 타입
function login(user: User) {
// Admin => 현재까지 {kickCount} 강퇴했습니다.
// Member => {point}명 모았습니다.
// Guest => {visitCount}번 방문했습니다.
switch (user.tag) {
case "ADMIN": {
console.log(`${user.kickCount}명 강퇴`);
break;
}
case "MEMBER": {
console.log(`${user.point} 모음`);
break;
}
case "GUEST": {
console.log(`${user.visitCount}번 방문`);
break;
}
}
}
서로소 유니온 타입을 활용하여 쉽게 타입을 좁히고 타입별 수행할 기능을 정의할 수 있다.
또 다른 예시를 보자. 아래의 코드는 비동기 작업의 결과를 처리하는 객체를 만든 것이다.
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);
}
}
}
본 포스팅은 인프런 한 입 크기로 잘라먹는 타입스크립트 강의를 기반으로 작성되었습니다. 사용된 이미지들은 모두 위 강의에서 가져왔습니다. 문제시 알려주세요.
'웹개발 > TypeScript' 카테고리의 다른 글
타입스크립트 | 인터페이스 (0) | 2023.08.24 |
---|---|
타입스크립트 | 함수와 타입 (0) | 2023.08.24 |
타입스크립트 | 기본 타입 정리 (1) | 2023.08.18 |
타입스크립트 | 컴파일러 옵션 설정하기 (0) | 2023.08.16 |
타입스크립트 | 기본 소개 (0) | 2023.08.16 |