TypeScript 고급 타입
1. 인터섹션 타입
두가지의 타입을 결합하여 다른 타입을 만들 수 있다.
type 사용
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
type ElevatedEmployee = Admin & Employee;
const e1: ElevatedEmployee = {
name: "Max",
privileges: ["create-server"],
startDate: new Date(),
};
interface 사용
interface Admin {
name: string;
privileges: string[];
};
interface Employee {
name: string;
startDate: Date;
};
interface ElevatedEmployee extends Admin, Employee {};
union 타입을 사용하여 인터섹션 타입을 만들면 두 가지 타입에서 공통된 타입만 인식한다.
type Combinable = string | number;
type Numeric = number | boolean;
type Universal = Combinable & Numeric;
2. 타입 가드
유니온 타입이 지닌 유연성을 활용할 수 있게 해준다.
특정 속성이나 메소드를 사용하기 전에 그것이 존재하는지 확인하거나 타입을 사용하기 전에 이 타입으로 어떤 작업을 수행할 수 있는지를 확인하는 개념 또는 방식을 나타내는 용어이다. 클래스의 경우 instanceof, 객체의 경우 in을 사용하여 수행할 수 있고 다른 타입들의 경우 typeof를 사용할 수 있다.
type priceType = string | number;
let totalPrice: number;
let itemPrice: priceType = "1000";
if (typeof itemPrice === "string") {
totalPrice = parseInt(itemPrice);
} else {
totalPrice = itemPrice;
}
전형적인 타입 가드 방법. typeof로 price가 string 인지 number 인지 판별 후 각 상태에 따라 다르게 처리하는 로직을 구현한다.
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
type UnknownEmployee = Employee | Admin;
function print(emp: UnknownEmployee) {
console.log(emp.name);
if ("privileges" in emp) {
console.log(emp.privileges);
}
if ("startData" in emp) {
console.log(emp.startData);
}
}
UnknownEmployee는 Employee 또는 Admin 타입을 가진다. 따라서 privileges가 있을 때, 없을 때가 있고, startDate가 있을 때, 없을 때가 있다. 해당 속성들에 접근하기 위해 자바스크립트의 in을 사용한다.
emp에 privileges가 없는 객체를 전달한다면 Admin 타입을 가졌으니 이름과 srateDate를 콘솔에 출력하게 되고, startDate가 없는 객체를 전달한다면 Employee 타입을 가져 이름과 privileges를 콘솔에 출력하게 된다.
instanceof
클래스의 타입을 감지하는 역할. 객체가 클래스에 기반하는지 확인할 수 있고, 클래스에 기반한다면 해당 클래스의 메소드를 가지고 있는지 확인할 수 있다. 인터페이스에서는 작동하지 않는데 인터페이스는 자바스크립트로 컴파일되지 않기 때문이다.
// Car와 Truck 각각의 class들... Truck에는 loadCargo라는 메소드 존재.
// Car와 Truck를 interface로 정의했다면 작동하지 않는다.
type Vehicle = Car | Truck;
function useVehicle(vehicle: Vehicle) {
if (vehicle instanceof Truck) {
vehicle.loadCargo(1000);
}
}
3. 구별된 유니언 (discriminated)
구별된 유니언을 구성하는 모든 객체에는 하나의 공통 속성만 있고 유니언은 해당 속성을 설명하므로 객체를 설명하는 이 속성을 switch 문에 사용하여 완전한 타입 안전성을 갖추고 객체에 어떤 속성을 사용할 수 있는지 파악할 수 있다. 객체와 유니언 타입을 사용한 작업 시 사용할 수 있는 아주 유용한 패턴이라고 한다.
// type도 가능
interface Bird {
type: 'bird';
flyingSpeed: number;
}
interface Horse {
type: 'horse';
runningSpeed: number;
}
type Animal = Bird | Horse;
function moveAnimal(animal: Animal) {
let speed;
switch (animal.type) {
case 'bird':
speed = animal.flyingSpeed;
break;
case 'horse':
speed = animal.runningSpeed;
}
console.log('Moving at speed: ' + speed);
}
moveAnimal({type: 'bird', flyingSpeed: 10});
4. 형 변환(typecasting)
타입스크립트가 직접 감지하지 못하는 특정 타입의 값을 타입스크립트에 알려주는 역할을 한다. 타입스크립트에 특정 값이 특정 타입임을 알리려고 하는 것.
dom에 있는 무언가로 접근할 때 (ex. const paragraph= document.querySelector('p')) 타입스크립트는 HTML파일을 분석하지 못하기 때문에 단순히 HTML요소나 null로 추론하게 된다.
만약 text타입의 input에 접근한 후 (const userInputElement=document.getElementById('user-input')) userInputElement.value에 새로운 값을 할당하려고 한다면 객체가 null일 수 있다는 에러가 발생하게 된다.
이 때 형 변환을 통해 에러를 해결할 수 있다.
const userInputElement = <HTMLInputElement>document.getElementById('user-input')!;
const userInputElement = document.getElementById('user-input')! as aHTMLInputElement;
userInputElement.value = "Hi, There!";
두 가지 방법 중 하나를 선택해서 사용하면 된다.
cf.) userInputElement의 느낌표는?
느낌표를 사용하여 느낌표 앞의 표현식을 null로 반환(yield)하지 않겠다고 타입스크립트에게 인식시킬 수 있다.
5. 인덱스 속성
객체가 지닐 수 있는 속성에 대해 보다 유연한 객체를 생성할 수 있게 해주는 기능.
사용자 입력의 유효성을 검사하는 애플리케이션을 작성하는 경우 여러 입력 필드가 필요할 것이며 사용자의 관심사가 무엇이며 어떤 필드인지에 따라 각기 다른 에러 메시지를 저장해서 보여주고자 한다면, 각 상황에 적합한 에러 메시지를 에러 컨테이너에 추가해야 한다.
interface ErrorContainer {
[prop: string]: string;
}
const errorBag: ErrorContainer = {
email: "Not a valid email!",
username: "Must start with a capital character!",
};
인덱스 타입을 키:string : 값:string으로 정했기 때문에 만들어지는 객체의 키, 값은 이를 따라야 한다.
6. 함수 오버로드
동일한 함수에 대해 여러 함수 시그니처를 정의할 수 있는 기능으로, 다양한 매개변수를 지닌 함수를 호출하는 여러 가지 가능한 방법을 사용하여 함수 내에서 작업을 수행할 수 있게 해준다.
타입스크립트가 자체적으로 반환 타입을 정확히 추론하지 못하는 경우에 유용하며 함수에서 지원할 수 있는 다양한 조합에 대해 어떤 것이 반환되는지 명확하게 알 수 있다.
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: string | number, b: string | number) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
7. 선택적 체이닝
백엔드에서 데이터를 가져오고 데이터베이스나 객체 내 특정 속성이 정의되어 있는지 확실하지 않은 소스에서 데이터를 가져오는 애플리케이션이 있다고 가정했을 때, 정의되어 있는지 여부가 확실치 않은 요소 다음에 물음표를 추가하여 에러를 막는 방법이다.
const fetchUserData = {
id: "u1",
name: "Max",
job: { title: "CEO", description: "My own company" },
};
console.log(fetchUserData?.job?.title);
백엔드 데이터에서 가져온 fetchUserData에서 job이 없는 경우 에러가 발생하게 되는데, 물음표를 통해 정의되어 있는지의 여부를 확인한다고 보면 된다. 이 부분은 데이터에 접근하기 전에 데이터의 존재 여부를 확인하는 if 문으로 컴파일된다.
8. Null 병합
어떤 데이터나 입력값이 있는데 그것이 null 인지, undefined 인지, 유효한 데이터인지 알 수 없는 경우 폴백(fallback) 값 (디폴트 값...이라고 생각하면 될 것 같다) 을 저장하고자 할 때, || 연산자를 사용하면 폴백 값이 할당되게 된다.
const userInput = " ";
const storedData = userInput || "DEFAULT";
// 이 경우 storeData는 빈 문자열을 null로 인식해 DEFAULT 값을 가지게 된다.
??는 null 병합 연산자로, 빈 문자열이나 0이 아닌 null이나 undefined 둘 중 하나라면 폴백을 사용하고 싶을 때 사용하면 된다.