동그란 도그린
[타입스크립트] 한 입 크기로 잘라먹는 타입스크립트 정리 (6. 클래스 ~ 10. 유틸리티 타입) 본문
6️⃣ 클래스
✨ 클래스란?
- 동일한 모양의 객체를 더 쉽게 생성하도록 도와주는 문법
- 붕어빵 = 객체, 붕어빵 기계 = 클래스
- 클래스를 이용하여 새로운 객체를 생성할 때 new 클래스이름 형태로 클래스의 생성자 함수 호출
class Student {
name;
age;
grade;
// 생성자
constructor(name, age, grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
// 메서드
study() {
console.log("열심히 공부함");
}
}
// 클래스 호출하여 객체 생성
const studentA = new Student("홍길동", 27, "A+");
✨ 상속
- 앞서 만든 클래스를 기반으로 추가적인 필드와 메서드를 갖는 클래스를 선언하고 싶다면 상속 이용
- 상속할 클래스의 생성자를 함께 호출해줘야 함 ⇒ 호출하지 않으면 이전 클래스에서 정의한 필드가 제대로 설정되지 않음
// Student 클래스 상속
class StudentDeveloper extends Student {
// 필드
favoriteSkill;
// 생성자
constructor(name, age, grade, favoriteSkill) {
this.favoriteSkill = favoriteSkill;
}
// 메서드
programming() {
console.log(`${this.favoriteSkill}로 프로그래밍함`);
}
}
✨ 타입스크립트의 클래스
- 클래스의 필드를 선언할 때 타입 주석으로 타입을 함께 정의해야 함 (정의하지 않으면 암시적 any 타입으로 추론되는데 strict 옵션이 true일 때는 오류가 발생)
- 생성자에서 각 필드의 값을 초기화하지 않을 경우, 초기값도 함께 명시해줘야 함
- 생성자 함수에서 필드의 값들을 잘 초기화해준다면 필드 선언 시의 초기값은 생략해도 됨
- 클래스가 생성하는 객체의 특정 프로퍼티를 선택적 프로퍼티를 만들고 싶다면 필드 이름 뒤에 물음표 붙이면 됨
class Employee {
// 필드
name: string = "";
age: number = 0;
position?: string = ""; **// ✨ 선택적 프로퍼티 (물음표 붙임)**
// 생성자
constructor(name: string, age: number, position: string) {
this.name = name;
this.age = age;
this.position = position;
}
// 메서드
work() {
console.log("일함");
}
}
- 타입스크립트의 클래스는 타입으로도 사용 가능
class Employee {
(...)
}
const employeeC: Employee = { **// ✨ 타입으로 사용**
name: "",
age: 0,
position: "",
work() {},
};
✨ 타입스크립트의 상속
- 파생 클래스에서 생성자를 정의했다면 반드시 super 메서드를 호출하여 슈퍼 클래스의 생성자를 호출해야 함
- 슈퍼 클래스의 생성자 호출 위치는 생성자의 최상단이어야 함
class ExecutiveOfficer extends Employee {
officeNumber: number;
constructor(
name: string,
age: number,
position: string,
officeNumber: number
) {
super(name, age, position); **// ✨ 슈퍼 클래스의 생성자 호출**
this.officeNumber = officeNumber;
}
}
✨ 접근 제어자
- 타입스크립트에서만 제공되는 기능
- 클래스의 특정 필드나 메서드를 접근할 수 있는 범위를 설정하는 기능
- 3개의 접근 제어자 사용 가능
- public : 모든 범위에서 접근 가능
- private : 클래스 내부에서만 접근 가능
- protected : 클래스 내부 또는 파생 클래스 내부에서만 접근 가능
1. public
- 필드의 접근 제어자를 지정하지 않으면 기본적으로 public 접근 제어자를 갖게 됨
class Employee {
// 필드
public name: string; **// ✨ public 접근 제어자를 직접 명시도 가능**
public age: number;
public position: string;
...
}
const employee = new Employee("이정환", 27, "devloper");
employee.name = "홍길동";
employee.age = 30;
employee.position = "디자이너";
2. private
class Employee {
// 필드
private name: string; **// ✨ private 접근 제어자 설정**
public age: number;
public position: string;
...
// 메서드
work() {
console.log(`${this.name}이 일함`); // 여기서는 접근 가능
}
}
const employee = new Employee("이정환", 27, "devloper");
employee.name = "홍길동"; **// ❌ 오류 (클래스 내부에서만 접근 가능)**
employee.age = 30;
employee.position = "디자이너";
3. protected
class Employee {
// 필드
private name: string;
protected age: number; **// ✨ protected 접근 제어자 설정**
public position: string;
...
// 메서드
work() {
console.log(`${this.name}이 일함`);
}
}
class ExecutiveOfficer extends Employee {
// 메서드
func() {
this.name;
this.age; **// ✅ 가능 (파생 클래스에서 접근 가능)**
}
}
const employee = new Employee("이정환", 27, "devloper");
employee.name = "홍길동";
employee.age = 30; **// ❌ 오류 (클래스 내부나 파생 클래스에서 접근 가능)**
employee.position = "디자이너";
✨ 접근 제어자 사용 ⇒ 필드 생략
- 생성자에 접근 제어자를 설정하면 동일한 이름의 필드를 선언하지 못하게 됨 (접근 제어자가 설정되면 자동으로 필드도 함께 선언되기 때문) ⇒ 중복된 필드 선언을 모두 제거
- 접근 제어자가 설정된 매개변수들은 this.필드 = 매개변수가 자동으로 수행됨 ⇒ 아래와 같은 생성자 내부 코드 제거 가능
- 💡 클래스 사용 시 보통 생성자 매개변수에 접근 제어자를 설정하여 필드 선언과 생성자 내부 코드를 생략하는 것이 훨씬 간결하고 빠르게 코드를 작성할 수 있어 좋음
class Employee {
// 필드
private name: string; **// ❌ ⇒ 중복 제거**
protected age: number; **// ❌ ⇒ 중복 제거**
public position: string; **// ❌ ⇒ 중복 제거**
// 생성자
constructor(
private name: string,
protected age: number,
public position: string
) {
this.name = name; **// 제거 가능**
this.age = age; **// 제거 가능**
this.position = position; **// 제거 가능**
}
// 메서드
work() {
console.log(`${this.name} 일함`);
}
}
✨ 인터페이스를 구현하는 클래스
- 인터페이스를 이용해 클래스에 어떤 필드/메서드들이 존재하는지 정의 가능
**interface** CharacterInterface {
name: string;
moveSpeed: number;
move(): void;
}
class Character **implements CharacterInterface** {
constructor(
public name: string,
public moveSpeed: number,
private extra: string
) {}
move(): void {
console.log(`${this.moveSpeed} 속도로 이동!`);
}
}
7️⃣ 제네릭
✨ 제네릭 함수란?
- 모든 타입의 값을 다 적용할 수 있는 범용적인 함수
- 타입 변수 T에 어떤 타입이 할당될지는 함수가 호출될 때 결정됨
- 제네릭 함수 호출 시 타입 변수에 할당할 타입을 직접 명시할 수도 있음
- 타입 변수가 2개 필요하다면 T, U와 같이 2개의 타입 변수 사용 가능
function func<T>(value: T): T {
return value;
}
let num = func(10);
let arr = func<[number, number, number]>([1, 2, 3]); **// ✨ 할당할 타입 직접 명시도 가능**
✨ 제네릭 함수 사용 사례
function swap<T, U>(a: T, b: U) {
return [b, a];
}
const [a, b] = swap("1", 2); **// T는 String 타입, U는 Number 타입으로 추론됨**
// 다양한 배열 타입을 인수로 받는 제네릭 함수
function returnFirstValue<T>(data: T[]) {
return data[0];
}
let str = retrunFirstValue([1, "hello", "mynameis"]);
let num = returnFirstValue([0, 1, 2]);
**// 반환값의 타입을 배열의 첫번째 요소의 타입이 되도록 하려면 튜플 타입과 나머지 파라미터 이용**
function returnFirstValue<T>(data: [T, ...unknown[]]) {
return data[0];
}
let str = returnFirstValue([1, "hello", "mynameis"]); **// number 타입으로 추론됨**
❗ 제네릭 함수 이용 ⇒ Map 메서드 타입 정의
- map 메서드는 모든 타입의 배열에 적용할 수 있으므로 arr 타입은 unknown[]
- callback의 타입은 배열 요소 하나를 매개변수로 받아 특정 값을 반환하는 함수로 정의
- unknown 타입을 타입 변수 T로 대체
// 1단계
function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}
// 2단계
function map<T>(arr: T[], callback: (item: T) => T): T[] {}
// 3단계 (map 메서드는 원본 배열 타입과 다른 타입의 배열로도 변환 가능해야 함)
function map<T, U>(arr: T[], callback: (item: T) => U): U[] {}
✨ 제네릭 인터페이스
- 제네릭 인터페이스는 제네릭 함수와 달리 변수의 타입으로 정의할 때 반드시 꺽쇠와 함께 타입 변수에 할당할 타입을 명시해야 함 (제네릭 함수는 매개변수 값의 타입을 기준으로 타입 변수의 타입을 추론할 수 있지만, 인터페이스는 마땅히 추론할 수 있는 값이 없기 때문)
**interface** KeyPair<K, V> {
key: K;
value: V;
}
let keyPair: KeyPair<string, number> = {
key: "key",
value: 0,
};
// 인덱스 시그니쳐와 함께 사용 => 더 유연한 객체 타입 정의 가능
**interface** Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
✨ 제네릭 타입 별칭
**type** Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "string",
};
✨ 제네릭 클래스
class List<T> {
constructor(private list: T[]) {}
push(data: T) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const numberList = new List([1, 2, 3]);
const stringList = new List(["1", "2"]);
✨ Promise 사용
- Promise는 제네릭 클래스로 구현되어 있음
- 새로운 Promise를 생성할 때 타입 변수에 할당할 타입을 직접 설정하면 해당 타입이 resolve 결과값의 타입이 됨
8️⃣ 타입 조작하기
✨ 타입 조작이란?
- 기본 타입이나 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 것
- 타입스크립트에서는 제네릭 이외에도 다양한 타입 조작 기능을 제공
1. 인덱스드 액세스 타입
- 객체 프로퍼티의 타입 추출하기
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number; **// 추가**
};
}
function printAuthorInfo(author: { id: number; name: string, age: number }) {
**// age 프로퍼티도 추가 => 프로퍼티 타입이 수정되면 매개변수 타입도 계속 수정해줘야 하는 불편함**
console.log(`${author.id} - ${author.name}`);
}
// 위와 같은 불편함을 해결하기 위해 Post 타입으로부터 author 프로퍼티 타입을 추출
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number; // 추가
};
}
function printAuthorInfo(author: Post["author"]) { **// author 프로퍼티 타입을 추출**
console.log(`${author.id} - ${author.name}`);
}
- 배열 요소의 타입 추출하기
// 배열 타입에서 하나의 요소의 타입만 추출할 수 있음
const post: PostList[number] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "이정환",
age: 27,
},
};
- 튜플 요소 타입 추출하기
2. Keyof 연산자
interface Person {
name: string;
age: number;
}
function getPropertyKey(person: Person, key: "name" | "age") { // Person 타입에 새로운 프로퍼티가 추가되거나 수정될 때마다 key 타입 수정해줘야 함
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
interface Person {
name: string;
age: number;
location: string; // 추가
}
function getPropertyKey(person: Person, key: keyof Person) { // keyof 연산자 이용
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
- Typeof와 Keyof 연산자 함께 사용하기
function getPropertyKey(person: Person, key: keyof typeof person) { // typeof와 keyof 함께 사용
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
3. Mapped(맵드) 타입
4. 템플릿 리터럴 타입
9️⃣ 조건부 타입
✨ 조건부 타입이란?
- extends와 삼항 연산자를 이용해 조건에 따라 각각 다른 타입을 정의하도록 돕는 문법
type A = number extends string ? number : string;
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? number : string; // ObjB는 ObjA의 서브타입이므로 참이 되어 B는 number 타입이 됨
✨ 분산적인 조건부 타입
- 타입 변수에 할당한 Union 타입 내부의 모든 타입이 분리됨
- 이후 분산된 각 타입의 결과를 모아 다시 Union 타입으로 묶음
type StringNumberSwitch<T> = T extends number ? string : number;
(...)
let c: StringNumberSwitch<number | string>;
// c변수는 string | number 타입으로 정의됨
// 조건부 타입의 타입 변수에 Union 타입을 할당하면 분산적인 조건부 타입으로 조건부 타입이 업그레이드 되기 때문
✨ Exclude 조건부 타입 구현하기
- Union 타입이 분리됨
- Exclude<number, string>
- Exclude<string, string>
- Exclude<boolean, string>
- 분리된 타입 모두 계산
- 계산된 타입들을 모두 Union으로 묶음
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>;
✨ infer
- 조건부 타입 내에서 특정 타입을 추론하는 문법
- 특정 함수 타입에서 반환값의 타입만 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 이용 가능
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>;
// string
type B = ReturnType<FuncB>;
// number
🔟 유틸리티 타입
✨ 유틸리티 타입이란?
- 제네릭, 맵드 타입, 조건부 타입 등의 타입 조작 기능을 이용해 실무에서 자주 사용되는 유용한 타입들을 모아 놓은 것
✨ Partial<T>
- 특정 객체 타입의 모든 프로퍼티를 선택적 프로퍼티로 변환
- 기존 객체 타입에 정의된 프로퍼티들 중 일부분만 사용할 수 있도록 도와주는 타입
✨ Required<T>
- 특정 객체 타입의 모든 프로퍼티를 필수(선택적이지 않은) 프로퍼티로 변환
✨ Readonly
- 특정 객체 타입의 모든 프로퍼티를 읽기 전용 프로퍼티로 변환
✨ Pick<T, K>
- 특정 객체 타입으로부터 특정 프로퍼티만 골라내는 타입
✨ Omit<T, K>
- 특정 객체 타입으로부터 특정 프로퍼티만 제거하는 타입
✨ Exclude<T, K>
- T로부터 K를 제거하는 타입
✨ Extract<T, K>
- T로부터 K를 추출하는 타입
✨ ReturnType<T>
- 타입변수 T에 할당된 함수 타입의 반환값 타입을 추출하는 타입
'한 입 크기로 잘라먹는 타입스크립트' 핸드북을 정리한 내용입니다.
'FrontEnd' 카테고리의 다른 글
[타입스크립트] 한 입 크기로 잘라먹는 타입스크립트 정리 (1. 타입스크립트 개론 ~ 5. 인터페이스) (0) | 2023.10.07 |
---|
Comments