728x90
- 타입스크립트는 어떤 기준으로 타입을 정의하는가?
- 타입 스크립트는 어떤 기준으로 타입 간의 관계를 정의하는가?
- 타입 스크립트는 어떤 기준으로 타입의 오류를 검사하는가?
타입 스크립트 문법만 정리된 Cheat Sheets도 있지만 원리를 이해해야 정확히 사용할 수 있다.
타입은 집합이다
- 동일한 속성과 특징을 갖는 여러 개의 값들을 모아둔 집합
- let num: 20 = 20; 의 20은 넘버 리터럴 타입이지만 동시에 넘버 타입에 속하기도 한다.
- 이와 같이 타입들끼리 어떤 계층을 갖게 된다.
- 타입 간의 호환성은 어떤 타입을 다른 타입으로 취급해도 괜찮은지 판단하는 것
- 넘버 리터럴 타입의 값인 20을 넘버 타입의 값으로 취급해도 되지만, 반대는 불가능하다. (ex. 직사각형과 정사각형의 관계)
let num1: number = 10;
let num2: 10 = 10;
num1 = num2; // 가능
num2 = num1; // 불가능
- 슈퍼 타입(부모 타입)의 값을 서브 타입(자식 타입)의 값으로 보는 것을 다운 캐스팅이라고 한다. (대부분 불가능)
- 서브 타입(자식 타입)의 값을 슈퍼 타입(부모 타입)의 값으로 보는 것을 업 캐스팅이라고 한다. (모든 상황에 가능)
타입 계층도와 함께 기본 타입 살펴보기
// 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 boo: boolean = unknownVar;
}
- unknown 타입은 모든 타입의 전체 집합 격으로, 모든 타입이 unknown 타입으로 업 캐스팅 될 수 있다.
- 반면 unknown 타입이 서브 타입으로 다운 캐스팅 될 수는 없다.
// never 타입 (불가능, 모순을 의미 - 공집합 & 모든 집합의 부분 집합)
function neverExam() {
// 반환값이 아무 것도 없다
function neverFunc(): never {
while (true) {}
}
// 업 캐스팅
let num: number = neverFunc();
let str: string = neverFunc();
let boo: boolean = neverFunc();
// 다운 캐스팅은 안됨
// let never1: never = 10;
// let never2: never = 'string';
// let never3: never = true;
}
- 모든 집합의 부분 집합, 가장 아래의 서브 집합인 never는 불가능, 모순을 뜻하며 반환값이 존재할 수 없는 경우 사용한다.
- 가장 아래의 서브 집합이기 때문에 위의 집합인 number, string 등 다른 슈퍼 집합으로 업 캐스팅이 가능하다.
- 하지만 슈퍼 집합이 never로 다운 캐스팅될 수는 없다.
// void 타입 (반환값이 없는 경우. undefined의 슈퍼 타입임)
function voidExam() {
function voidFunc(): void {
console.log('hi');
// return undefined (가능)
}
let voidVar: void = undefined;
}
- 반환값이 존재할 수 없는 never와 달리 void는 그냥 '없는' 경우를 의미한다.
- undefined의 슈퍼 타입이기 때문에 undefined가 void로 업 캐스팅 될 수 있다.
// any 타입 (타입 계층 무시. 모든 타입의 슈퍼 타입이자 모든 타입의 서브 타입임(never 제외))
function anyExam() {
let unknownVar: unknown;
let anyVar: any;
let undefinedVar: undefined;
let neverVar: never;
anyVar = unknownVar; // any 타입에 한정해서 unknown 타입이 다운 캐스팅 됨
undefinedVar = anyVar; // any 타입은 자기한테 오는 다운 캐스팅, 자기가 다운 캐스팅도 됨
// neverVar = anyVar; // 이건 안됨 (never는 순수한 공집합이기 때문에 어떤 타입도 다운 캐스팅 안됨)
}
- any 타입은 모든 타입의 슈퍼 타입이자 모든 타입의 서브 타입이다. (never 제외)
- 단, any 타입에 한정해서 unknown 타입이 다운 캐스팅 될 수 있다.
- 단, never 타입으로는 어떤 타입도 다운 캐스팅 될 수 없기 때문에 any 역시 다운 캐스팅 될 수 없다.
객체 타입의 호환성
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; // Property 'breed' is missing in type 'Animal' but required in type 'Dog'.
- 객체는 프로퍼티를 기준으로 계층 기준을 갖는다.
- 동일한 프로퍼티를 가졌다고 할 때 조건이 더 적은 쪽이 슈퍼 타입(부모 타입)이 된다.
let animal2: Animal = {
name: '몽구',
color: 'black',
// breed: '포메라니안'
};
let animal3: Animal = dog;
function func(animal: Animal) {}
func({
name: '몽구',
color: 'black',
// breed: '포메라니안'
})
func(dog);
- 변수를 초기화 할 때 객체 리터럴을 쓰면 발동되는 초과 프로퍼티 검사
- 실제 타입에 정의하지 않은 초과 프로퍼티를 적어 놓으면 에러가 발생한다.
- 객체 리터럴로 초기화 한다면 실제 타입과 동일하게 프로퍼티를 설정해야 한다.
- 함수의 매개변수로 추가할 때에도 에러가 발생하는 점은 동일하다.
대수 타입 (Algebraic Data Type = ADT)
- 여러 개의 타입을 합성해서 새롭게 만들어낸 타입
- 합집합, 교집합 타입
// 합집합 (Union)
let a: string | number;
a = 10;
a = 'a';
let arr: (number | string | boolean)[] = [1, "hello", true]
- string number union type
- 숫자형, 문자형 타입의 값을 할당 받을 수 있는 합집합 타입
- |으로 원하는 만큼 타입을 추가할 수 있다.
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: "",
// }
- Dog 타입과 Person 타입을 선언한 후 그 둘의 합집합인 Union1 타입을 선언했다.
- Union1 타입에 부합할 수 있는 경우는 union1, union2, union3으로 union4는 부합하지 않는다.
// 교집합 (Intersection)
let variable: number & string;
type Intersection = Dog & Person;
- 단순 변수의 경우 number와 string이 교집합이 될 수 없으므로 never가 된다.
- 따라서 주로 객체에서 많이 사용하는데, Dog 타입과 Person 타입의 교집합으로 Intersection 타입을 선언한다.
let intersection1: Intersection = {
name: "",
color: "",
language: ""
}
- Intersection 타입의 변수 intersection1은 Dog 타입과 Person 타입의 프로퍼티를 모두 가져야 한다.
타입 추론 (Type Inference)
- 점진적 타입 추론: 변수의 초기값으로 변수의 타입을 추론하는 타입 스크립트의 시스템
- 객체나 배열이 구조 분해 할당이 되어도, 객체 안에 여러 타입의 값이 있어도 자동으로 타입을 추론해준다.
- 하지만 함수를 정의할 때 함수 파라미터의 타입을 지정해주지 않으면 에러가 발생한다. (어떤 타입의 값이 들어오는지 모름)
- 단, 파라미터의 초기값이 있으면 그 초기값을 기준으로 추론한다.
let d;
d = 10;
d.toFixed();
d = "hi";
d.toUpperCase();
// d.toFixed(); // 에러 발생
- let d는 초기값이 없으므로 암묵적으로 타입을 any로 추론한다.
- 변수 d에 들어가는 값에 따라 any가 다른 타입으로 진화하는 것을 any 타입의 진화라고 한다.
- 명시적으로 let d: any를 하는 것과는 다름 (타입이 계속 진화하므로 d.toFixed() 같은 이전 타입은 쓸 수 없음)
- 다만 이렇게 타입이 계속 진화하면 혼란의 여지가 있다.
const num = 10;
const str = 'hi';
- let이 아닌 const로 선언한 num의 타입은 number가 아니라 넘버 리터럴 10이 된다. (상수이기 때문에)
- str의 타입도 단순 string이 아니라 'hi'를 타입으로 갖는 스트링 리터럴이 된다.
- const로 선언된 경우가 아니라면 타입 스크립트는 리터럴이 아닌 number, string처럼 범용적으로 타입을 추론한다. (타입 넓히기)
타입 단언
type Person = {
name: string;
age: number;
};
let person = {} as Person;
person.name = 'summermong';
person.age = 27;
- 변수 person을 Person 타입의 값으로 하고 싶지만 처음엔 {}으로 선언한 뒤 속성을 추가하고 싶은 경우가 있다고 하자.
- let person: Person = {} 으로 코드를 작성하면 Person 타입의 값이 아니라고 에러가 발생한다.
- let person = {}으로 초기화를 하면 이것도 이것대로 객체 자체에는 name, age라는 속성이 없기 때문에 또 에러가 발생한다.
- 따라서 {} 이지만 Person이 될 것이라고 단언하는 것을 타입 단언(Type Assertion)이라고 한다.
- Person 타입으로 단언된 초기화 값으로 타입이 추론되기 때문에 person은 Person 타입으로 추론된다.
type Dog = {
name: string;
color: string;
};
let dog = {
name: '주몽',
color: 'brown',
breed: '시츄'
} as Dog;
- 변수 dog의 타입을 Dog로 설정하면 breed 때문에 초과 프로퍼티 검사에 걸리게 된다.
- 이 때 as Dog를 작성해 Dog 타입으로 단언해주면 에러가 발생하지 않는다.
let num1 = 10 as never; // number는 never의 슈퍼 타입
let num2 = 10 as unknown; // number는 unknown의 서브 타입
// let num3 = 10 as string; // number는 string의 슈퍼/서브 타입이 아님
let num3 = 10 as unknown as string; // 다중단언으로 가능함
- 단, 타입 선언에는 규칙이 있다.
- 값 as 단언 일 때, 값은 단언하는 것의 슈퍼 타입 또는 서브 타입이어야 한다.
- 다중단언으로 서로소와 같은 타입이어도 타입을 단언할 수는 있으나 지양하는 것이 좋다.
let num4 = 10 as const;
- as const를 작성하지 않을 때 num4의 타입은 number로 추론된다.
- const로 타입을 단언하면 넘버 리터럴 타입 10으로 추론된다. (=const로 선언한 것과 동일한 효과)
let cat = {
name: '야옹이',
color: 'yellow'
} as const;
cat.name = '' // readonly
- const 단언은 객체와 사용하면 객체의 속성을 수정할 수 없도록 readonly 키워드를 붙여준다.
- 일일이 readonly 키워드를 붙일 필요가 없고, 속성이 많아 변경될 위험이 있는 객체의 경우도 안전하게 속성값을 보존할 수 있다.
type Post = {
title: string;
author?: string;
};
let post: Post = {
title: '1',
author: 'summermong',
};
const len: number = post.author.length;
// const len: number = post?.author.length; // 옵셔널 체이닝
const len: number = post.author!.length;
- 어떤 값이 undefined거나 null이 아니라고 단언하는 Non Null 단언
- author가 string일수도 있고 없을 수도 있을 때, length와 같이 점 표기법으로 접근할 때 자동으로 ?.(옵셔널 체이닝)이 생긴다.
- 옵셔널 체이닝이란 접근한 프로퍼티의 값이 undefined거나 null일 때 undefined로 바꿔주는 역할을 한다.
- 따라서 이 값이 undefined나 null이 아니라 string이라고 단언하기 위해서 !.(단언 연산자)을 붙여주면 된다.
- 단언은 업 캐스팅이나 다운 캐스팅처럼 실제로 바꾸는 것이 아니라 잠깐 컴파일러의 눈을 가린 것으로 사용 시 주의해야 한다.
타입 좁히기
function func(value: number | string) {
value; // number | string
// value.toUpperCase();
// value.toFixed();
if (typeof value === 'number') {
console.log(value.toFixed()); // number
} else if (typeof value === 'string') {
console.log(value.toUpperCase()); // string
}
}
- 조건문 등을 이용해 타입을 상황에 따라 좁히는 방법을 타입 좁히기(Type Narrowing)라고 한다.
- func의 매개변수인 value는 number 또는 string이기 때문에 조건문 밖에서는 number, string 메서드를 쓸 수 없다.
- 하지만 조건문으로 value의 타입이 number, 또는 string이 될 때는 타입 스크립트가 타입을 좁혀 판단해 에러가 발생하지 않는다.
- 조건문의 중괄호에서 타입을 좁힐 수 있는데 이렇게 타입을 지키는 부분을 타입 가드(Type Guard)라고 한다.
function func(value: number | string | Date | null) {
if (typeof value === 'number') {
console.log(value.toFixed()); // number
} else if (typeof value === 'string') {
console.log(value.toUpperCase()); // string
} else if (typeof value === 'object') {
console.log(value.getTime()); // object를 좀 더 좁혀야함
}
}
- Date 객체를 단순히 object로 좁히면 null에서 에러가 발생한다. (typeof null은 객체를 반환한다.)
- 따라서 아래와 같이 instanceof 연산자로 value는 Date 클래스의 인스턴스인지 확인하는 방식으로 수정할 수 있다.
type Person = {
name: string;
age: number;
};
function func(value: number | string | Date | null | Person) {
if (typeof value === 'number') {
console.log(value.toFixed()); // number
} else if (typeof value === 'string') {
console.log(value.toUpperCase()); // string
} else if (value instanceof Date) {
console.log(value.getTime()); // object를 좀 더 좁혀야함 null도 통과함
} else if (value && 'age' in value) {
console.log(`${value.name}은 ${value.age}살입니다.`)
}
}
- value가 Person 타입의 값이라고 할 때, value instanceof Person을 사용하면 에러가 발생한다.
- instanceof는 우측에 작성된 것이 해당 클래스의 '인스턴스'인지 확인하는 연산자다.
- Person은 그저 타입일 뿐이지 클래스의 인스턴스가 아니기 때문에 사용할 수 없다.
- 이런 경우에는 in으로 해당 프로퍼티를 갖고 있는지 확인할 수 있다. (age 프로퍼티를 value가 가지고 있는지)
- 다만 이 때 가지고 있지 않다, 즉 null일 수 있기 때문에 value가 있다고 조건을 하나 더 추가해주면 된다.
서로소 유니온 타입 (Taged Union Type)
type Admin = {
name: string;
kickCount: number;
};
type Member = {
name: string;
point: number;
};
type Guest = {
name: string;
visitCount: number;
};
type User = Admin | Member | Guest;
- name만 겹치고 나머지 프로퍼티는 다른 세 가지 타입이 있다고 할 때, User가 어떤 타입인지 확인해 로그인을 하려고 한다.
function login(user: User) {
if ('kickCount' in user) {
console.log(`${user.name}님 현재까지 ${user.kickCount}명 강퇴했습니다.`);
} else if ('point' in user) {
console.log(`${user.name}님 현재까지 ${user.point}를 모았습니다.`);
} else {
console.log(`${user.name}님 현재까지 ${user.visitCount}번 방문했습니다.`);
}
}
- 가장 처음엔 in 연산자로 각 프로퍼티에 해당되는 부분으로 타입을 좁힐 수 있다.
- 하지만 이렇게 하면 코드가 직관적이지 않다는 단점이 있다.
type Admin = {
tag: 'ADMIN';
name: string;
kickCount: number;
};
type Member = {
tag: 'MEMBER';
name: string;
point: number;
};
type Guest = {
tag: 'GUEST';
name: string;
visitCount: number;
};
- 이 때 tag 프로퍼티로 각각의 타입을 스트링 리터럴로 적어주면 세 가지 타입을 묶은 User는 서로소 유니온이 된다.
function login(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 login(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}번 방문했습니다.`);
}
}
}
- 그렇다면 이렇게 좀 더 직관적으로 코드를 짤 수 있다.
// 비동기 작업의 결과를 처리하는 객체
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}`);
break;
}
}
}
**출처: 한 입 크기로 잘라먹는 타입스크립트 (인프런, 이정환 강사님)
728x90
'❔ TypeScript > 💭 한 입 크기 TypeScript' 카테고리의 다른 글
[TypeScript] 인터페이스 (0) | 2023.12.20 |
---|---|
[TypeScript] 함수 타입 (1) | 2023.12.19 |
[TypeScript] 타입스크립트의 기본 타입 정리 (0) | 2023.12.16 |
[TypeScript] 타입스크립트 실행 및 컴파일러 옵션 설정 (0) | 2023.09.08 |
[TypeScript] 타입 스크립트의 등장과 동작 원리 (0) | 2023.09.08 |