섹션6. 고급 타입
1) 고급 타입
(1) intersection
다른 타입을 결합할 수 있는 타입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Admin {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
}
// 두 타입의 결합된 하나의 타입
type ElevatedEmployee = Admin & Employee
const e1: ElevatedEmployee = {
name: "Coco",
privileges: ["create-server"],
startDate: new Date()
}
type Combinnable = string | number;
type Numeric = number | boolean;
// 타입스크립트는 Universal을 숫자형 타입으로 간주한다.
// Combinnable과 Numeric이 겹치는 타입은 숫자형 타입뿐이기 때문이다.
type Universal = Combinnable & Numeric
(2) index
객체가 지닐 수 있는 속성에 대해 보다 유연한 객체를 생성할 수 있게 해준다.
유연성을 제공하여 사용하고자 하는 속성 이름과 필요한 속성의 개수를 미리 알 필요가 없다.
예를 들어, 사용자 입력의 유효성을 검사하는 애플리케이션을 작성하는 경우 여러 입력 필드가 필요하며 입력필드가 계속 추가되거나 변경될 수 있다. 이런 경우 어떤 내용을 포함하는 지에 대해 굉장히 유연해야 한다. 이런 경우에 인덱스 타입을 사용할 수 있다.
1
2
3
4
5
6
7
8
9
// 대괄호 쌍을 이용하여 인덱스 타입을 정의한다.
interface ErrorContainer { // { email: 'Not a valid email', username: 'Must start with a character!' }
[prop: string]: string;
}
const errorBag: ErrorContainer = {
email: 'Not a valid email!',
username: 'Must start with a capital character!'
};
위와 같이, 대괄호 쌍을 사용하여 인덱스 타입을 정의하면 후에 해당 인터페이스를 사용할 때 속성과 값이 정의한 타입이기만 하면 된다.
(3) 함수 오버로드
동일한 함수에 대해 여러 함수 시그니처를 정의할 수 있는 기능으로, 간단히 말해 다양한 매개변수를 지닌 함수를 호출하는 여러 가지 가능한 방법을 사용하여 함수 내에서 작업을 수행할 수 있게 해준다.
1
2
3
4
5
6
7
8
9
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
const result = add('Max', ' Schwarz');
result.split(' '); // 에러 발생
위와 같은 에러가 발생하는 이유는 명확하다. 함수의 반환 값인 result가 string | number
와 같은 유니언 타입이기 때문에 string에서 사용할 수 있는 메서드를 사용하면 에러가 발생한다.
그래서 이처럼 타입스크립트가 자체적으로 반환 타입을 정확히 추론하지 못하는 경우, 함수 오버로드를 사용한다. 함수에서 지원할 수 있는 다양한 조합에 대해 어떤 것이 반환되는지 명확하게 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 함수 오버로드: 주요 함수 바로 위에 같은 이릅을 입력한다.
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
const result = add('Max', ' Schwarz');
result.split(' ');
타입스크립트는 함수오버로드와 함수 선언을 하나로 병합하여 해당 라인들을 통해 알게된 정보를 결합한다. 즉, 두 문자열을 지닌 add를 호출했지만 타입스크립트는 add를 호출하는 방법이 세 가지 더 있음을 타입스크립트는 알게 된다.
(4) 선택적 체이닝
객체 데이터의 중첩된 속성과 객체에 안전하게 접근할 수 있게 한다. (참조하는 값이 없더라도 문제가 발생하지 않도록 함 )
예를 들어, 백엔드에서 데이터를 가져오고 데이터 베이스나 객체 내 특정 속성이 정의되어 있는지 확실하지 않은 소스에서 데이터를 가져오는 경우 데이터를 가져오면서 필요한 데이터를 모두 다 가져오지 못할 수 있다. 특히 백엔드와 통신할 때, 규모가 크고 복잡한 애플리케이션에서 이 시점에 일부 데이터가 설정되지 않는 경우가 종종 있다. 이런 상황에서 사용되는 것이 선택적 체이닝이다.
1
2
3
4
5
6
7
const fetchedUserData = {
id: 'u1',
name: 'Max',
// job: { title: 'CEO', description: 'My own company' }
};
console.log(fetchedUserData.job.title) // 런타임시 에러 발생
위와 같이 객체의 일부 속성이 설정되어 있는지 또는 정의되지 않았는지 확실히 알 수 없는 데이터를 가져올 수 없는 경우 당연히 에러가 발생한다.
문제는 타입스크립트가 다음과 같은 내용을 확실히 알 수 없다는 것이다.
- 타입스크립트에서 제어되지 않는 일부 파일에서 가져오는 데이터가 있는지
- 백엔드에서 가져오는 데이터가 어떻게 반환될지
기존 자바스크립트의 선택적 체이닝
1
2
3
4
5
6
7
const fetchedUserData = {
id: 'u1',
name: 'Max',
job: { title: 'CEO', description: 'My own company' }
};
console.log(fetchedUserData.job && fetchedUserData.job.title);
이는 잠재적인 객체를 자세히 살펴보기에 앞서 객체가 존재하는지 여부를 확인하는 자바스크립트의 방식으로 이 부분이 정의되지 않으면 이 코드는 실행되지 않으므로 런타임 에러를 피할 수 있다. 존재하지 않더라도 적어도 런타임 에러는 발생하지 않지만, 타입스크립트로 더 나은 방법을 사용할 수 있다.
타입스크립트의 선택적 체이닝
1
2
3
4
5
6
7
8
const fetchedUserData = {
id: 'u1',
name: 'Max',
job: { title: 'CEO', description: 'My own company' }
};
// 정의되어 있는지 여부가 확실치 않은 요소 다음에 물음표를 추가한다.
console.log(fetchedUserData?.job?.title);
물음표의 역할은 이 부분이 job에 접근하는 경우 해당 부분이 존재하는지를 알려준다. 따라서 물음표를 추가하여 job이 정의되어 있는 경우 title에 접근할 수 있다.사실상 이는 데이터에 접근하기 전에 데이터의 존재 여부를 확인하는 if 문으로 컴파일되어 작동한다.
(5) Null 병합
null 데이터 처리에 도움을 준다.
만약 해당 값이 null
이거나 undefined
이면 fallback 값을 저장한다고 하면, 다음과 같이 논리적연산자로 구현할 수 있다.
1
2
3
const userInput = undefined;
const storeData1= userInput || 'DEFAULT;
console.log(storedData);
논리적 OR 연산자로 DEFAULT가 저장되도록 구현하여 첫 번째 값이 undefined거나 거짓 같은 값이면 null이면 DEFALUT
를 저장한다. 그러나 이 방법의 문제는 이것이 null이나 undefined가 아닌 빈 문자열이더라도 거짓 같은 값으로 처리되어 기본 폴백 값이 적용된된다. 이런 경우 타입스크립트의 ??
를 활용하면 해결이 가능하다.
1
2
const userInput = undefined;
const storedData = userInput ?? 'DEFAULT';
이는 즉, userInput
값이 빈 문자열이나 0이 아닌 null이나 undefined 둘 중 하나라면 폴백을 사용해야 한다는 의미이다. null이나 undefined가 아닌 이상 해당 값을 사용하는 것으로 이중 물음표 연산자를 활용하면 null이나 undefined를 매끄럽게 처리하는 데 도움이 된다.
2) 타입가드
타입가드는 특정 속성이나 메소드를 사용하기 전에 그것이 존재하는지 확인하거나 타입을 사용하기 전에 이 타입으로 어떤 작업을 수행할 수 있는지를
확인하는 개념 또는 방식을 나타내는 용어로 런타입 시 특정 타입으로 작업을 수행하기 전에 해당 타입을 검사하는 코드 패턴이다.
(1) typeof
이는 유니온 타입이 지닌 유연성을 활용할 수 있게 해주며 런타임 시 코드가 정확하게 작동하게 해주며, 객체가 아닌 타입들의 경우 일반적으로 typeof
를 사용하여 타입가드를 구현한다.
1
2
3
4
5
6
7
8
9
type Combinable = string | number;
function add(a: Combinable, b: Combinable) {
// typeof를 활용한 타입가드: 맞는 타입인지 확인한다.
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
위의 코드에서 if절이 없다면, 원하는 결과가 나오지 않을 수 있다. if절이 유니온 타입의 세부 분기를 지정해주어 원하는 결과를 도출하도록 만든다.
(2) in
객체에서 타입가드를 구현하기 위해 사용된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
// in을 활용한 타입가드: 해당 객체안에 privileges 속성이 있는지 확인한다.
if ('privileges' in emp) {
console.log('Privileges: ' + emp.privileges);
};
if ('startDate' in emp) {
console.log('Start Date: ' + emp.startDate);
};
};
printEmployeeInformation({ name: 'Manu', startDate: new Date() });
in
연산자를 활용하여 객체 안에 속성이 있는지 없는지 확인하여 함수의 분기를 나눌 수 있다.
(3) instanceof
객체의 타입가드를 구현하는 또 다른 방법 중 하나이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Car {
drive() {
console.log('Driving...');
}
}
class Truck {
drive() {
console.log('Driving a truck...');
}
loadCargo(amount: number) {
console.log('Loading cargo ...' + amount);
}
}
type Vehicle = Car | Truck;
const v1 = new Car();
const v2 = new Truck();
function useVehicle(vehicle: Vehicle) {
vehicle.drive();
// instanceof를 활용한 타입 가드: Truck 객체를 참조하고 있는지 확인한다.
if (vehicle instanceof Truck) {
vehicle.loadCargo(1000);
}
객체의 경우 instanceof나 in을 사용하여 수행할 수 있고 다른 타입들의 경우 typeof를 사용할 수 있다.
3) 형 변환
타입스크립트가 직접 감지하지 못하는 특정 타입의 값을 타입스크립트에 알려주는 역할을 한다.
예를 들어, dom에 접근하는 경우 타입스크립트는 해당 값에 대한 타입을 감지할 수 없다. 타입스크립느는 HTML파일을 살펴보고 분석하지 못하기 때문에 해당 값이 실제로 존재하는 지 알 수 없기 때문이다. (HTML 코드를 읽을 수 없기 때문에 어떤 HTML 요소라고만 인식을 한다. ) 이를 해결하기 위해 우린 타입을 직접 설정해줄 수 있다.
(1) <>
를 요소의 앞에 적는다
1
2
3
4
//<타입>을 요소의 앞에 기입하여 타입스크립트에게 해당 요소의 타입을 알려준다.
const userInputElement = <HTMLInputElement>document.getElementById('user-input')!;
userInputElement.value = 'Hi there!';
<HTMLInputElement>
를 타입을 설정할 요소 앞에 기입함으로, 해당 요소가 HTML의 Input타입인 것을 알려준다. 요소 뒤에 붙은 !
는 해당 값이 NULL이 아니라고 확정하는 것으로 NULL을 반환하지 않을 것을 안다면 이 느낌표를 사용할 수 있다.
(2) as를 요소의 뒤에 적는다.
1
2
3
4
// 요소의 뒤에 as 타입을 기입하여 타입스크립트에게 해당 요소의 타입을 알려준다.
const userInputElement = document.getElementById('user-input')! as HTMLInputElement;
userInputElement.value = 'Hi there!';
as HTMLInputElement
를 통해 해당 요소의 타입이 HTML의 Input타입인 것을 알려준다.
현재 !
를 통해 해당 요소가 NULL이 없다는 것을 가정했지만, 만약 null을 반환할 수 있는 경우에는 다음과 같이 코드를 수정해야 한다.
1
2
3
4
5
6
7
// 확신할 수 없기 떄문에 형변환을 하지 못한다.
const userInputElement = document.getElementById('user-input');
// 만약 null값이 아니면, HTMLInputELement 타입이된다.
if (userInputElement) {
(userInputElement as HTMLInputElement).value = 'Hi there!';
}