타입 관련 고급 기능
1. 타입 단언과 널 처리
1.1 Non-Null 단언 연산자
JavaScript에서는 DOM 요소를 선택할 때 해당 요소가 없을 수 있어서 null
을 반환할 수 있습니다. TypeScript에서는 이런 상황에서 !
연산자를 사용하여 "이 값은 절대 null이 아닙니다"라고 컴파일러에게 알려줄 수 있습니다.
// 기본적인 예제
const $button = document.querySelector('button');
// 이 시점에서 TypeScript는 $button이 null일 수 있다고 생각합니다
// $button.click(); // 에러 발생!
// Non-Null 단언 사용
const $safeButton = document.querySelector('button')!;
$safeButton.click(); // 정상 작동
// 기본적인 예제
const $button = document.querySelector('button');
// 이 시점에서 TypeScript는 $button이 null일 수 있다고 생각합니다
// $button.click(); // 에러 발생!
// Non-Null 단언 사용
const $safeButton = document.querySelector('button')!;
$safeButton.click(); // 정상 작동
1.2 타입 단언(Type Assertion)
때로는 우리가 TypeScript보다 어떤 값의 타입을 더 정확히 알고 있는 경우가 있습니다. 이럴 때 as
키워드를 사용하여 타입을 명시적으로 지정할 수 있습니다. 다만 타입 단언은 매우 주의해야 하는데, 타입 단언을 할 경우 런타임과 컴파일 모두 유형 검사를 우회하게 됩니다. 따라서 타입 단언을 남발하면 타입 안전성이 떨어질 수 있습니다.
그러면 타입 단언은 언제, 왜 사용해야 할까요? 타입 단언을 사용하게 되면 사람과 컴퓨터 모두가 타입을 인식하기 좋습니다. 예를 들어, IDE 같은 경우 자동완성 기능을 제공할 때 타입 단언을 사용하면 더 정확한 정보를 제공할 수 있습니다. 사람이 읽을 때에도 타입 단언을 사용하면 코드를 더 쉽게 이해할 수 있습니다.
// 이벤트 핸들러 예제
document.querySelector('input')!.addEventListener('change', (e) => {
// e.target은 기본적으로 EventTarget 타입
const target = e.target as HTMLInputElement;
console.log(target.value); // .value 접근 가능
// as 없이 작성한다면:
// const target = e.target;
// console.log(target.value); // 에러 발생!
});
// 실제 사용 예제
interface User {
name: string;
age: number;
}
const userJSON = '{"name": "John", "age": 30}';
const user = JSON.parse(userJSON) as User;
console.log(user.name); // 타입 안전성 확보
// 이벤트 핸들러 예제
document.querySelector('input')!.addEventListener('change', (e) => {
// e.target은 기본적으로 EventTarget 타입
const target = e.target as HTMLInputElement;
console.log(target.value); // .value 접근 가능
// as 없이 작성한다면:
// const target = e.target;
// console.log(target.value); // 에러 발생!
});
// 실제 사용 예제
interface User {
name: string;
age: number;
}
const userJSON = '{"name": "John", "age": 30}';
const user = JSON.parse(userJSON) as User;
console.log(user.name); // 타입 안전성 확보
💡 주의사항: 타입 단언은 필요한 경우에만 사용해야 합니다. 과도한 사용은 타입 안전성을 해칠 수 있습니다.
2. 객체 타입 고급 기능
2.1 인덱스 시그니처
객체의 속성 이름을 미리 알 수 없을 때 유용한 기능입니다. 주로 동적 데이터를 다룰 때 사용됩니다.
// 기본 예제
interface Dictionary {
[key: string]: string;
}
const colors: Dictionary = {
red: "#ff0000",
blue: "#0000ff",
green: "#00ff00"
};
// 실제 활용 예제
interface UserData {
id: number;
name: string;
[key: string]: string | number | boolean; // 다양한 타입 허용
}
const userData: UserData = {
id: 1,
name: "Alice",
age: 25,
isAdmin: true,
department: "Engineering"
};
// 기본 예제
interface Dictionary {
[key: string]: string;
}
const colors: Dictionary = {
red: "#ff0000",
blue: "#0000ff",
green: "#00ff00"
};
// 실제 활용 예제
interface UserData {
id: number;
name: string;
[key: string]: string | number | boolean; // 다양한 타입 허용
}
const userData: UserData = {
id: 1,
name: "Alice",
age: 25,
isAdmin: true,
department: "Engineering"
};
2.2 keyof 연산자
객체의 모든 키를 타입으로 추출할 때 사용합니다. 특히 제네릭과 함께 사용할 때 유용합니다. 이 연산자를 사용하면 객체의 키를 문자열로 추출할 수 있습니다. 간단한 예제부터 살펴보도록 하겠습니다.
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"
const data: ProductKeys = "id";
// 여기에 "name", "price", "category" 등 다른 값을 사용할 수 있습니다.
// 다만 1000, "hello" 등의 key가 아닌 경우에는 사용할 수 없습니다.
console.log(data); // id
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"
const data: ProductKeys = "id";
// 여기에 "name", "price", "category" 등 다른 값을 사용할 수 있습니다.
// 다만 1000, "hello" 등의 key가 아닌 경우에는 사용할 수 없습니다.
console.log(data); // id
이는 아래와 같은 방식으로 사용할 수 있게 해줍니다.
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"
// 실제 활용 예제
function getProductValue(product: Product, key: keyof Product) {
return product[key];
}
const laptop: Product = {
id: 1,
name: "노트북",
price: 1000000,
category: "전자기기"
};
console.log(getProductValue(laptop, "name")); // "노트북"
console.log(getProductValue(laptop, "invalid")); // 컴파일 에러!
interface Product {
id: number;
name: string;
price: number;
category: string;
}
// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"
// 실제 활용 예제
function getProductValue(product: Product, key: keyof Product) {
return product[key];
}
const laptop: Product = {
id: 1,
name: "노트북",
price: 1000000,
category: "전자기기"
};
console.log(getProductValue(laptop, "name")); // "노트북"
console.log(getProductValue(laptop, "invalid")); // 컴파일 에러!
다만 이렇게 사용했을 경우 일반적으로 laptop["name"]
과 같이 직접 접근하는 것이 더 간단할 수 있습니다. 하지만 keyof 연산자는 복잡한 타입에서 사용할 때 유용합니다.
3. 연습문제
-
Non-Null 단언 문제
// TODO: Non-Null 단언을 사용하여 아래 코드를 완성하세요. const $input = document.querySelector('input'); // $input의 value를 콘솔에 출력하세요.
// TODO: Non-Null 단언을 사용하여 아래 코드를 완성하세요. const $input = document.querySelector('input'); // $input의 value를 콘솔에 출력하세요.
-
타입 단언 문제
// TODO: 아래 JSON 데이터를 파싱하고 타입 단언을 사용하여 // name과 age를 안전하게 접근할 수 있도록 만드세요. const data = '{"name": "Jane", "age": 25}';
// TODO: 아래 JSON 데이터를 파싱하고 타입 단언을 사용하여 // name과 age를 안전하게 접근할 수 있도록 만드세요. const data = '{"name": "Jane", "age": 25}';
-
인덱스 시그니처 문제
// TODO: 학생의 과목별 성적을 저장할 수 있는 인터페이스를 만드세요 // 학생의 이름은 필수이며, 나머지 과목들은 동적으로 추가될 수 있습니다
// TODO: 학생의 과목별 성적을 저장할 수 있는 인터페이스를 만드세요 // 학생의 이름은 필수이며, 나머지 과목들은 동적으로 추가될 수 있습니다
-
keyof 연산자 문제
interface Car { brand: string; model: string; year: number; } // TODO: Car 인터페이스의 속성 중 하나를 인자로 받아 // 해당 속성의 값을 반환하는 함수를 작성하세요.
interface Car { brand: string; model: string; year: number; } // TODO: Car 인터페이스의 속성 중 하나를 인자로 받아 // 해당 속성의 값을 반환하는 함수를 작성하세요.
4. 연습문제 해답
-
Non-Null 단언 해답
const $input = document.querySelector('input')!; console.log($input.value);
const $input = document.querySelector('input')!; console.log($input.value);
-
타입 단언 해답
interface Person { name: string; age: number; } const data = '{"name": "Jane", "age": 25}'; const person = JSON.parse(data) as Person; console.log(person.name, person.age);
interface Person { name: string; age: number; } const data = '{"name": "Jane", "age": 25}'; const person = JSON.parse(data) as Person; console.log(person.name, person.age);
-
인덱스 시그니처 해답
interface StudentGrades { name: string; [subject: string]: string | number; // 과목명: 점수 } const studentGrades: StudentGrades = { name: "김철수", math: 95, english: 88, science: 92 };
interface StudentGrades { name: string; [subject: string]: string | number; // 과목명: 점수 } const studentGrades: StudentGrades = { name: "김철수", math: 95, english: 88, science: 92 };
-
keyof 연산자 해답
interface Car { brand: string; model: string; year: number; } function getCarProperty(car: Car, property: keyof Car) { return car[property]; } const myCar: Car = { brand: "Cats Motors", model: "Licat1", year: 2224 }; console.log(getCarProperty(myCar, "brand")); // "Cats Motors" console.log(myCar['brand']); // "Cats Motors" // 간단한 타입의 경우에는 myCar['brand']와 같이 직접 접근해도 크게 문제가 없지만 // 복잡한 타입의 경우에는 keyof를 사용하는 것이 안전합니다.
interface Car { brand: string; model: string; year: number; } function getCarProperty(car: Car, property: keyof Car) { return car[property]; } const myCar: Car = { brand: "Cats Motors", model: "Licat1", year: 2224 }; console.log(getCarProperty(myCar, "brand")); // "Cats Motors" console.log(myCar['brand']); // "Cats Motors" // 간단한 타입의 경우에는 myCar['brand']와 같이 직접 접근해도 크게 문제가 없지만 // 복잡한 타입의 경우에는 keyof를 사용하는 것이 안전합니다.