![[Typescript] 클래스(2)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhAca9%2FbtsMEqcnnw1%2FuEUrJiS12fhw4hqqLyNws0%2Fimg.png)
5️⃣ 클래스 확장
TypeScript의 클래스는 다른 클래스를 확장하거나 하위 클래스를 만드는 JavaScript 개념에 타입 검사를 추가한다. 즉, JavaScript에서의 클래스 + 타입검사가 TypeScript에서의 클래스 확장성에 대한 개념이다.
먼저 당연하게도 기본 클래스에 선언된 모든 메서드나 속성은 파생 클래스라고 불리는 하위 클래스에서 사용할 수 있다.
class Teacher{
teach(){
console.log("Teaching");
}
}
class StudentTeacher extends Teacher{
learn(){
console.log("Learning");
}
}
const teacher = new StudentTeacher();
teacher.teach(); /* OK */
teacher.learn(); /* OK */
5️⃣- 1. 할당 가능성 확장
파생 인터페이스가 기본 인터페이스를 확장하는 것과 마찬가지로 하위 클래스도 기본 클래스의 멤버를 상속한다.
class Lesson {
subject : string;
constructor(subject : string){
this.subject = subject;
}
}
class OnlineLesson extends Lesson {
url : string;
constructor(subject:string,url:string){
super(subject) /* call the constructor of the parent class */
this.url = url;
}
}
/* 부모 클래스 to 자식 클래스 */
let lesson : Lesson = new Lesson("Math");
lesson = new OnlineLesson("Math","http://www.math.com");
/* 자식 클래스 to 부모 클래스 : 부모 클래스는 자식 클래스의 인스턴스를 사용할 수 없음*/
let online : OnlineLesson;
online = new OnlineLesson("conding","http://www.code.com");
online = new Lesson("coding"); /* Error: Type 'Lesson' is not assignable to type 'OnlineLesson'. */
대부분 실제상황에서 하위 클래스는 일반적으로 기본 클래스 위에 새로운 필수 타입 정보를 추가한다.
5️⃣- 2. 재정의된 생성자
Vanila JavaScript와 마찬가지로 TypeScript에서 하위 클래스는 자체 생성자를 정의할 필요가 없다. 자체 생성자가 없는 상속된 클래스는 기본적으로 부모 생성자를 사용한다.
볼드화된 부분을 텍스트로만 보면 조금 이상하게 느껴진다. 이런 경우는 어떨까?
조부모 클래스 → 부모 클래스 → 자식 클래스로 상속이 이어졌다고 가정해보자. 또한, 조부모 클래스와 부모 클래스는 각각 생성자를 가지고 있다.
그러면 자식 클래스는 자체 생성자가 없기 때문에 기본적으로 부모 생성자를 사용하는데....이 때 조부모 클래스의 생성자를 사용할까? 아니면 부모 클래스의 생성자를 사용할까?
class GrandParent {
constructor() {
console.log("👴 GrandParent 생성자 호출");
}
}
class Parent extends GrandParent {
constructor() {
super(); /* 👴 조부모 생성자 호출 */
console.log("👨 Parent 생성자 호출");
}
}
class Child extends Parent {
/* 생성자 없음 → 부모 클래스(Parent)의 생성자가 자동 호출됨 */
}
const child = new Child();
출력을 해보면 아래와 같이 나온다.
즉, Child는 직속 부모 클래스(Parent)의 생성자를 거쳐, 최종적으로 조부모(GrandParent) 생성자를 실행하는 구조이다.
또한, 하위(자식) 클래스에서 자체 생성자를 정의하려면 super 키워드를 통해 기본 클래스 생성자를 반드시 호출해야한다.
하위 클래스 생성자는 기본 클래스에서의 필요 여부와 상관 없이 모든 매개변수를 선언할 수 있다.
5️⃣- 3. 재정의된 메서드 : 메서드 오버라이딩
하위 클래스의 메서드가 기본 클래스의 메서드에 할당할 수 있는 한 하위 클래스는 기본 클래스와 동일한 이름으로 새 메서드를 다시 선언할 수 있다.
이 때 메서드 오버라이딩은 두 가지 조건을 충족해야한다.
- 매개변수의 개수
- 반환타입
class GradeCounter {
countGrades(grades: string[], letter: string) {
return grades.filter(grade => grade === letter).length;
}
}
class FailureCounter extends GradeCounter {
countGrades(grades: string[]) {
return super.countGrades(grades, 'F'); /* 부모 메서드 호출 시 'F'를 기본값으로 전달 */
}
}
자세히 보면 서로 다른 두 클래스의 메서드의 매개변수의 개수가 다르다. 원칙적으론 매개변수의 개수가 같아야하는데 TypeScript는 이걸 허용하고 있다.
✅ TypeScript에서 매개변수 개수가 다른 오버라이딩이 허용되는 이유
TypeScript는 메서드 오버로딩(overloading)과 다형성(polymorphism)을 지원하기 때문에, 자식 클래스의 메서드가 부모 클래스보다 적은 매개변수를 받을 수 있다.
이건 매개변수의 기본값(default value) 또는 메서드의 범용성 때문이다.
💡 왜 오류가 나지 않을까?
자식 클래스에서 countGrades(grades: string[])를 정의할 때, 부모의 countGrades(grades: string[], letter: string)를 완전히 대체하지 않는다.
- 즉, 부모의 메서드 시그니처를 변경한 것이 아니라, 특정한 경우에 letter를 ‘F’로 고정한 새로운 메서드를 만든 것이다.
- TypeScript에서는 부모의 메서드보다 더 적은 인자를 받는 메서드로 오버라이딩하는 것을 허용한다.
부모의 메서드를 super.countGrades(grades, 'F')로 호출하고 있기 때문에, 결국 부모 메서드의 형태를 유지하면서도 ‘F’를 기본값으로 사용할 수 있다.
🚀 이 방식이 허용되는 이유 (TypeScript의 오버라이딩 규칙)
TypeScript에서는 “⭐자식 클래스의 메서드는 부모 메서드보다 더 많은 매개변수를 받을 수 없지만, 더 적은 매개변수를 받을 수는 있다.”
이걸 “함수 타입의 공변성(Function Parameter Covariance)”이라고 한다.
🤝🏻 즉, 아래 두 가지 규칙을 만족하면 허용된다.
- 부모 메서드보다 더 적은 매개변수를 받을 수 있다. (countGrades(grades: string[]) → 부모보다 매개변수 하나 적음)
- 부모 메서드가 기대하는 매개변수를 내부적으로 채워서(super.countGrades(grades, 'F')), 일관된 동작을 유지할 수 있음.
5️⃣- 4. 재정의된 속성
하위 클래스는 새 타입을 기본 클래스의 타입에 할당할 수 있는 한 동일한 이름으로 기본 클래스의 속성을 명시적으로 다시 선언할 수 있다.
재정의된 메서드와 마찬가지로 하위 클래스는 근본(?) 클래스와 구조적으로 일치해야한다.
하위 클래스에서 속성을 다시 선언한다면...
1. 유니언 타입의 더 구체적인 하위 집합으로 만듦 : Narrowing: string | null → string
class Animal {
name: string | null = null; /*name은 string 또는 null일 수 있음*/
}
class Dog extends Animal {
name: string = "Buddy"; /* name을 string으로 좁힘 (null이 아님)*/
}
let myDog: Dog = new Dog();
console.log(myDog.name); /* "Buddy"*/
2. 기본 클래스 속성 타입에서 확장되는 타입으로 만듦 : Widening: "user" → "user" | "admin"
class Person {
role: "user" = "user"; /* role은 "user"라는 리터럴 타입 */
}
class Admin extends Person {
role: "user" | "admin" = "admin"; /* 더 넓은 유니언 타입으로 변경 */
}
let admin: Admin = new Admin();
console.log(admin.role); /* "admin" */
6️⃣추상 클래스
일부 메서드의 구현을 선언하지 않고 하위 클래스가 해당 메서드를 제공할 것을 예상하고 기본 클래스를 만드는 방법이다.
밑줄 친 문장 자체가 "추상화" 를 의미한다.
추상화하려는 클래스 이름과 메서드 앞에 abstract 키워드를 추가하여 사용한다.
추상화 메서드 선언은 추상화된 기본 클래스에서 메서드의 본문을 제공하지 않으며 인터페이스처럼 선언된다.
abstract class School{
readonly name:string;
constructor(name:string){
this.name = name;
}
abstract getStudentTypes() : string[];
}
class Preschool extends School{
getStudentTypes(){
return ["preschooler"];
}
}
class Absence extends School {};
/* Error
비추상 클래스 'Absence'은(는) 'School' 클래스에서 상속된 추상 멤버 'getStudentTypes'을(를) 구현하지 않습니다.ts(2515)
*/
✅추상 클래스는 클래스의 세부사항이 충족될 것 같은 프레임워크에서 자주 사용된다.
7️⃣멤버 접근성
JavaScript에서는 클래스 멤버 이름 앞에 "#" 을 붙여서 private 클래스 멤버임을 나타낸다.
당연하게도 private 클래스 멤버는 해당 클래스 인스턴스에서만 접근할 수 있으며 외부에서 접근하려 할 경우 오류를 발생시킨다.
🟦TypeScript는 private 클래스 멤버(#)를 지원함과 동시에 타입시스템에서 존재하는 프라이버시 정의 집합을 허용한다.
- 🎈public(Default) : 누구나 접근 가능
- 🎈protected : 클래스 내부 또는 하위 클래스에서만 접근 가능
- 🎈private : 클래스 내부에서만 접근 가능
class Animal {
public name: string; /* public: 어디서든 접근 가능 */
private age: number; /* private: 해당 클래스 내에서만 접근 가능 */
protected species: string; /* protected: 해당 클래스와 상속받은 클래스에서만 접근 가능 */
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this.species = species;
}
public greet() {
/* public 메서드: 어디서든 호출 가능 */
console.log(`Hello, I'm a ${this.species} named ${this.name}.`);
}
private getAge() {
/* private 메서드: 해당 클래스 내에서만 호출 가능 */
return this.age;
}
protected getSpecies() {
/* protected 메서드: 해당 클래스와 상속받은 클래스에서만 호출 가능 */
return this.species;
}
}
class Dog extends Animal {
constructor(name: string, age: number, species: string) {
super(name, age, species);
}
public greetDog() {
/* 상속받은 클래스에서 접근 가능한 protected 멤버에 접근 가능 */
console.log(`Woof! I'm a ${this.species} named ${this.name}.`);
}
public getDogSpecies() {
/* protected 메서드는 하위 클래스에서 접근 가능 */
return this.getSpecies();
}
/* private 멤버에는 접근할 수 없음 */
public getAge() {
return this.getAge(); /* 오류: 'getAge'는 private이므로 접근할 수 없습니다. */
}
}
const animal = new Animal('Elephant', 10, 'Mammal');
animal.greet(); /* 정상 작동 */
animal.getAge(); /* 오류: 'getAge'는 private이므로 접근할 수 없습니다. */
const dog = new Dog('Buddy', 5, 'Dog');
dog.greetDog(); /* 정상 작동 */
console.log(dog.getDogSpecies()); /* 정상 작동 */
dog.getAge(); /* 오류: 'getAge'는 private이므로 접근할 수 없습니다. */
7️⃣- 1. 정적 필드 제한자
JavaScript는 static 키워드를 사용해 클래스 자체에서 멤버를 선언한다.
TypeScript는 static 키워드를 단독으로 사용하거나 readonly와 접근성 키워드를 함께 사용할 수 있도록 지원한다.
class Question {
protected static readonly answer : "bash";
protected static readonly question : "What is the best shell?";
guess(getAnswer : (prompt:string) => string){
const answer = getAnswer(Question.question);
if(answer === Question.answer){
console.log("Correct!");
}
else{
console.log("Incorrect!");
}
}
}
Question.answer;
/*
-Error-
Property 'answer' is protected and only accessible within class 'Question' and its subclasses.
'answer' 속성은 보호된 속성이며 'Question' 클래스 및 해당 하위 클래스 내에서만 액세스할 수 있습니다.ts(2445)
*/
'FrontEnd > Typescript' 카테고리의 다른 글
[Typescript] 클래스(1) (2) | 2024.09.20 |
---|---|
[Typescript] 인터페이스(2) (0) | 2024.08.19 |
[Typescript] 인터페이스(1) (0) | 2024.08.19 |
[Typescript] 배열(2) (0) | 2024.08.11 |
[Typescript] 배열(1) (0) | 2024.08.05 |
안녕하세요? 개발자입니다.