[LearningTS] Chapter08. 클래스
포스트
취소

[LearningTS] Chapter08. 클래스

CHAPTER 08. 클래스

1
2
저는 일부 기능적 장치인 클래스를 절대 사용하지 않으려고 합니다.
저에게는 클래스가 너무 강렬해요!
  • 타입스크립트가 탄생되고 릴리스된 2010년 초반의 자바스크립트 세계는 오늘날과 상당히 달랐다.
  • 나중에 ES2015에서 표준화된 화살표 함수 let/const 변수 같은 기능은 아직 먼 희망이었다.
  • 바벨은 첫번째 커밋이 있은 후 몇 년이 흘렸고, 최신 자바스크립트 구문을 오래된 구문으로 변환하는 Traceur와 같은 이전 도구들을 완전히 주류로 채택 받지 못했다.
  • 타입스크립트의 초기 마케팅과 기능들은 이런 자바스크립트 세계에 맞춰져 있었다.
  • 타입스크립트의 타입 검사기 외에도 트랜스파일러가 강조되었고, 그 예시로 클래스가 자주 등장했다.
  • 오늘날 타입스크립트의 클래스 지원은 모든 자바스크립트 언어 기능을 지원하는 많은 기능 중 하나에 불과한다.
  • 타입스크립트는 클래스 사용이나 다른 인기 있는 자바스크립트 패턴을 권장하지도 막지도 않는다.


🌓 클래스 메서드

  • 타입스크립트는 독립 함수를 이해하는 것과 동일한 방식으로 메서드를 이해한다.
  • 매개변수 타입에 타입이나 기본값을 지정하지 않으면 any 타입을 기본으로 갖는다.
  • 메서드를 호출하려면 허용 가능한 수의 인수가 필요하고, 재귀 함수가 아니라면 대부분 반환 타입을 유추할 수 있다.

[예제 1]
string 타입의 단일 필수 매개변수를 갖는 greet 클래스를 가진 Greeter 클래스를 정의하는 코드다.

1
2
3
4
5
class Greeter {
  greet(name: string) {
    console.log(`${name}, do your stuff!`);
  }
}

  • 클래스 생성자는 매개변수와 관련하여 전형적인 클래스 메서드처럼 취급된다.
  • 타입스크립트는 메서드 호출 시 올바른 타입의 인수가 올바른 수로 제공되는지 확인하기 위해 타입검사를 수행한다.

[예제 2]
Greeted 생성자는 message: string으로 매개변수가 제공되어야 한다.

1
2
3
4
5
6
7
class Greeted {
  constructor(message: string) {
    console.log(`As I always say: ${message}`);
  }
}

new Greeted('take chances, make mistakes, get messy');


🌓 클래스 속성

  • 타입스크립트에서 클래스의 속성을 읽거나 쓰려면 클래스에 명시적으로 선언해야 한다.
  • 클래스 속성은 인터페이스와 동일한 구문을 사용해 선언한다.
  • 클래스 속성 이름 뒤에는 선택적으로 타입 애너테이션이 붙는다.
  • 타입스크립트는 생성자 내의 할당에 대해서 그 멤버가 클래스에 존재하는 멤버인지 추론하려고 시도하지 않는다.

[예제]

  • destination은 string으로 명시적으로 선언되어 있어 FieldTrip 클래스 인스턴스에 할당되고 접근할 수 있다.
  • 클래스가 nonexistent 속성을 선언하지 않았기 때문에 생성자에게 this.nonexistent 할당은 허용되지 않는다.

  • 클래스 속성을 명시적으로 선언하면 타입스크립트는 클래스 인스턴스에서 무엇이 허용되고, 허용되지 않는지 빠르게 이해할 수 있다.
  • 나중에 클래스 인스턴스가 사용될 때, 코드가 trip.nonexistent와 같은 클래스 인스턴스에 존재하지 않는 멤버에 접근하려고 시도하면 타입스크립트는 타입 오류를 발생시킨다.


1. 함수 속성

자바스크립트에는 클래스의 멤버를 호출 가능한 함수로 선언하는 두 가지 구문이 있다.

[예제 1]

  • myFunction(){}과 같이 멤버 이름 뒤에 괄호를 붙이는 메서드 접근 방식을 앞서 살펴보면, 메서드 메서드 접근 방식은 함수를 클래스 프로토타입에 할당하므로 모든 클래스 인스턴스는 동일한 함수 정의를 사용한다.
  • WithMethod 클래스는 모든 인스턴스가 참조할 수 있는 myMethod 메서드를 선언한다.
1
2
3
4
5
class WithMethod {
  myMethod() {}
}

new WithMethod().myMethod === new WithMethod().myMethod; // true
  • 값이 함수인 속성을 선언하는 방식도 있다.
  • 이렇게 하면 클래스의 인스턴스당 새로운 함수가 생성되며, 항상 클래스 인스턴스를 가리켜야 하는 화살표 함수에서 this 스코프를 사용하면 클래스 인스턴스당 새로운 함수를 생성하는 시간과 메모리 비용 측면에서 유용할 수 있다.

[예제 2]

1
2
3
4
5
class WithProperty {
  myProperty: () => {};
}

new WithProperty().myProperty === new WithProperty().myProperty; // false
  • 함수 속성에는 클래스 메서드와 독립 함수의 동일한 구문을 사용해 매개변수와 반환 타입을 지정할 수 있다.
  • 결국 함수 속성은 클래스 멤버로 할당된 값이고, 그 값은 함수다.

[예제 3]
WithPropertyParameters 클래스는 타입이 (input: boolean) => string인 takesParameters 속성을 가진다.

1
2
3
4
5
6
7
class WithPropertyParameters {
  takesParameters = (input: boolean) => (input ? 'Yes' : 'No');
}

const instance = new WithPropertyParameters();

instance.takesParameters(true); // Ok


2. 초기화 검사

  • 엄격한 컴파일러 설정이 활성화된 상태에서 타입스크립트는 undefined 타입으로 선언된 각 속성이 생성자에게 할당되었는지 확인한다.
  • 이와 같은 엄격한 초기화 검사는 클래스 속성에 값을 할당하지 않는 실수를 예방할 수 있어 유용하다.

[예제 1]
WithValue 클래스는 unused 속성에 값을 할당하지 않았고, 타입스크립트는 이 속성을 타입 오류로 인식한다.

엄격한 초기화 검사가 없다면, 비록 타입 시스템이 undefined 값에 접근할 수 없다고 말할지라도 클래스 인스턴스는 undefined 값에 접근할 수 있다.

[예제 2]
엄격한 초기화 검사가 수행되지 않으면 올바르게 컴파일되지만, 결과 자바스크립트는 런타임 시 문제가 발생한다.

1
2
3
4
5
6
class MissingInitializer {
  property: string;
}

new MissingInitializer().property.length;
// TypeError: Caanot read property 'length' of undefined


2.1. 확실하게 할당된 속성

  • 엄격한 초기화 검사가 유용한 경우가 대부분이지만 클래스 생성자 다음에 클래스 속성을 의도적으로 할당하지 않는 경우가 있을 수도 있다.
  • 엄격한 초기화 검사를 적용하면 안 되는 속성인 경우에는 이름 뒤에 !를 추가해 검사를 비활성화하도록 설정한다.
  • 이렇게 하면 타입스크립트에 속성이 처음 사용되기 전에 undefined 값이 할당된다.

[예제]
ActivitiesQueue 클래스는 생성자와는 별도로 여러 번 다시 초기화될 수 있으므로 pending 속성은 !와 함께 할당되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ActivitiesQueue {
  pending!: string[]; // Ok

  initialize(pending: string[]) {
    this.pending = pending;
  }

  next() {
    return this.pending.pop();
  }
}

const activities = new ActivitiesQueue();

activities.initialize(['eat', 'sleep', 'learn']);
activities.next();

🌿 클래스 속성에 대해 엄격한 초기화 검사를 비활성화하는 것은 종종 타입 검사에는 적합하지 않은 방식으로 코드가 설정된다는 신호다. ! 어서션을 추가하고 속성에 대한 타입 안정성을 줄이는 대신 클래스를 리팩터링해서 어서션이 필요하지 않도록 해야한다.


3. 선택적 속성

  • 인터페이스와 마찬가지로 클래스는 선언된 속성 이름 뒤에 ?를 추가해 속성을 옵션으로 선언한다.
  • 선택적 속성은 | undefined를 포함하는 유니언 타입과 거의 동일하게 작동한다.
  • 엄격한 초기화 검사는 생성자에서 선택적 속성을 명시적으로 설정하지 않아도 문제가 되지 않는다.

[예제]
MissingInitializer 클래스는 property를 옵션으로 정의했으므로 엄격한 속성 초기화 검사와 관계없이 클래스 생성자에서 할당하지 않아도 된다.

1
2
3
4
5
6
7
8
class MissingInitializer {
  property?: string;
}

new MissingInitializer().property?.length; // Ok

new MissingInitializer().property?.length;
// Error: Object is possibly 'undefined'.


4. 읽기 전용 속성

  • 인터페이스와 마찬가지로 선언된 속성 이름 앞에 readonly 키워드를 추가해 속성을 읽기 전용으로 선언한다.
  • readonly 키워드는 타입 시스템에만 존재하며 자바스크립트는 컴파일할 때 삭제된다.
  • readonly로 선언된 속성은 선언된 위치 또는 생성자에서 초깃값만 할당할 수 있다.
  • 클래스 내의 메서드를 포함한 다른 모든 위치에서 속성은 읽을 수만 있고, 쓸 수는 없다.

[예제 1]
Quote 클래스의 text 속성은 생성자에서는 값이 지정되지만 다른 곳에서 값을 지정하려고 하면 타입 오류가 발생한다.

  • 원시 타입의 초깃값을 갖는 readonly로 선언된 속성은 다른 속성과 조금 다르다.
  • 이런 속성은 더 넓은 원싯값이 아니라 값의 타입이 가능한 한 좁혀진 리터럴 타입으로 유추된다.
  • 타입스크립트는 값이 나중에 변경되지 않는다는 것을 알기 때문에 더 공격적인 초기 타입 내로잉을 편하게 느낀다.
  • const 변수가 let 변수보다 더 좁은 타입을 갖는 것과 유사하다.

[예제 2]
클래스 속성은 처음에는 모두 문자열 리터럴로 선언되므로 둘 중 하나를 string으로 확장하기 위해서는 타입 애너테이션이 필요하다.

  • 속성의 타입을 명시적으로 확장하는 작업이 자주 필요하지는 않는다.
  • 그럼에도 불구하고 RandomQuote에서 등장하는 생성자의 조건부 로직처럼 경우에 따라 유용할 수 있다.


🌓 타입으로서의 클래스

타입 시스템에서의 클래스는 클래스 선언이 런타임 값(클래스 자체)과 애너테이션에서 사용할 수 있는 타입을 모두 생성한다는 점에서 상대적으로 독특하다.

[예제 1]

  • Teacher 클래스의 이름은 teacher 변수에 주석을 다는 데 사용된다.
  • teacher 변수에는 Teacher 클래스의 자체 인스턴스처럼 Teacher 클래스에 할당할 수 있는 값만 할당해야 함을 타입스크립트에 알려준다.
1
2
3
4
5
6
7
8
9
class Teacher {
  sayHello() {
    console.log('Take chances, make mistakesm get messy!');
  }
}

let teacher: Teacher;

teacher = new Teacher(); // Ok

  • 타입스크립트는 클래스의 동일한 멤버를 모두 포함하는 모든 객체 타입을 클래스에 할당할 수 있는 것으로 간주한다.
  • 타입스크립트의 구조적 타이핑이 선언되는 방식이 아니라 객체의 형태만 고려하기 때문이다.

[예제 2]

  • withSchoolBus는 SchoolBus 타입의 매개변수를 받는다.
  • 매개변수로 SchoolBus 클래스 인스턴스처럼 타입이 () => string[]인 getAbilities 속성을 가진 모든 객체를 할당할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class SchoolBus {
  getAbilities() {
    return ['magic', 'shapeshifting'];
  }
}

function withSchoolBus(bus: SchoolBus) {
  console.log(bus.getAbilities());
}

withSchoolBus(new SchoolBus()); // Ok

// Ok
withSchoolBus({
  getAbilities: () => ['transmogrification'],
});

🌿 대부분의 실제 코드에서 우리는 클래스 타입을 요청하는 위치에 객체의 값을 전달하지 않는다. 이러한 구조적인 확인 동작은 예상하지 못하는 것처럼 보일 수 있지만 자주 나타나지는 않는다.


🌓 클래스와 인터페이스

  • 타입스크립트는 클래스 이름 뒤에 implements 키워드와 인터페이스 이름을 추가함으로써 클래스의 해당 인스턴스가 인터페이스를 준수한다고 선언할 수 있다.
  • 이렇게 하면 클래스를 각 인터페이스에 할당할 수 있어야 함을 타입스크립트에 나타낸다.
  • 타입 검사기에 의해 모든 불일치에 대해서 타입 오류가 발생한다.

[예제 1]
Student 클래스는 name 속성과 study 메서드를 포함해 Learner 인터페이스를 올바르게 구현했지만 Slaker에는 study가 누락되어 타입 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Learner {
  name: string;
  study(hours: number): void;
}

class Student implements Learner {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
  study(hours: number) {
    for (let i = 0; i < hours; i += 1) {
      console.log('...studyinh...');
    }
  }
}

  • 인터페이스를 구현하는 것으로 클래스를 만들어도 클래스가 사용되는 방식은 변경되지 않는다.
  • 클래스가 이미 인터페이스와 일치하는 경우 타입스크립트의 타입 검사기는 인터페이스의 인스턴스가 필요한 곳에서 해당 인스턴스를 사용할 수 있도록 해야한다.
  • 타입스크립트는 인터페이스에서 클래스의 메서드 또는 속성 타입을 유추하지 않는다.
  • Slacker 예제에서 study(hours){} 메서드를 추가했다면 타입스크립트는 타입 애너테이션을 지정하지 않는 한 hours 매개변수를 암시적 any로 간주한다.

[예제 2]
다른 형태로 구현한 Student 클래스는 멤버에 타입 애너테이션을 제공하지 않기 때문에 암시적인 any 타입 오류가 발생한다.

  • 인터페이스를 구현하는 것은 순전히 안정성 검사를 위해서다.
  • 모든 인터페이스 멤버를 클래스 정의로 복사하지 않는다.
  • 대신 인터페이스를 구현하면 클래스 인스턴스가 사용되는 곳에서 나중에 타입 검사기로 신호를 보내고 클래스 정의에서 표면적인 타입 오류가 발생한다.
  • 변수에 초깃값이 있더라도 타입 애너테이션을 추가하는 것과 용도가 비슷하다.


1. 다중 인터페이스 구현

  • 타입스크립트의 클래스는 다중 인터페이스를 구현해 선언할 수 있다.
  • 클래스에 구현된 인터페이스 목록은 인터페이스 이름 사이에 쉼표를 넣고, 개수 제한 없이 인터페이스를 사용할 수 있다.

[예제 1]

  • 두 클래스에서 모두 Graded를 구현하려면 grades 속성을 가져야 하고, Reporter를 구현하려면 report 속성을 가져야 한다.
  • Empty 클래스에는 Graded와 Reporter 인터페이스를 제대로 구현하지 못했으므로 두 가지 타입 오류가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Graded {
  grades: number[];
}

interface Reporter {
  report: () => string;
}

class ReportCard implements Graded, Reporter {
  grades: number[];

  constructor(grades: number[]) {
    this.grades = grades;
  }

  report() {
    return this.grades.join(',');
  }
}

  • 실제로 클래스가 한 번에 두 인터페이스를 구현할 수 없도록 정의하는 인터페이스가 있을 수 있다.
  • 두 개의 충돌하는 인터페이스를 구현하는 클래스를 선언하려고 하면 클래스에 하나 이상의 타입 오류가 발생한다.

[예제 2]

  • AgeIsNumber와 AgeIsNotNumber 인터페이스는 age 속성을 서로 다른 타입으로 선언한다.
  • AsNumber 클래스와 NotAsNumber 클래스 모두 두 인터페이스를 제대로 구현하지 못했다.
1
2
3
4
5
6
7
interface AgeIsNumber {
  age: number;
}

interface AgeIsNotNumber {
  age: () => string;
}

두 인터페이스가 매우 다른 객체 형태를 표현하려는 경우에는 동일한 클래스로 구현하지 않아야 한다.


🌓 클래스 확장

  • 타입스크립트는 다른 클래스를 확장하거나 하위 클래스를 만드는 자바스크립트 개념에 타입 검사를 추가한다.
  • 먼저 기본 클래스에 선언된 모든 메서드나 속성은 파생 클래스라고도 하는 하위 클래스에서 사용할 수 있다.

[예제]
Teacher는 StudentTeacher 하위 클래스의 인스턴스에서 사용할 수 있는 teach 메서드를 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Teacher {
  teach() {
    console.log('The surest test of discipline is its absence.');
  }
}

class StudentTeacher extends Teacher {
  learn() {
    console.log('I cannot afford the luxury of a closed mind.');
  }
}

const teacher = new StudentTeacher();

teacher.teach(); // Ok (기본 클래스에 정의됨)
teacher.learn(); // Ok (하위 클래스에 정의됨)


1. 할당 가능성 확장

  • 파생 인터페이스가 기본 인터페이스를 확장하는 것과 마찬가지로 하위 클래스도 기본 클래스의 멤버를 상속한다.
  • 하위 클래스의 인스턴스는 기본 클래스의 모든 멤버를 가지므로 기본 클래스의 인스턴스가 필요한 모든 곳에서 사용할 수 있다.
  • 만약 기본 클래스에 하위 클래스가 필요할 때 사용할 수 없다.

[예제 1]
Lesson 클래스의 인스턴스는 파생된 OnlineLesson 인스턴스가 필요한 곳에서 사용할 수 없지만, 파생된 인스턴스는 기본 클래스 또는 하위 클래스를 충족하는 데 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Lesson {
  subject: string;

  constructor(subject: string) {
    this.subject = subject;
  }
}

class OnlineLesson extends Lesson {
  url: string;

  constructor(subject: string, url: string) {
    super(subject);
    this.url = url;
  }
}

let lesson: Lesson;
lesson = new Lesson('coding'); // Ok
lesson = new OnlineLesson('coding', 'oreilly.com'); // Ok

let online: OnlineLesson;

online = new OnlineLesson('coding', 'oreilly.com'); // Ok

타입스크립트의 구조적 타입에 따라 하위 클래스의 모든 멤버가 동일한 타입의 기본 클래스에 이미 존재하는 경우 기본 클래스의 인스턴스를 하위 클래스 대신 사용할 수 있다.

[예제 2]
LabeledPastGrades는 선택적 속성인 PastGrades만 추가하므로 하위 클래스 대신 기본 클래스의 인스턴스를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
class PastGrades {
  grades: number[] = [];
}

class LabeledPastGrades extends PastGrades {
  label?: string;
}

let subClass: LabeledPastGrades;

subClass = new LabeledPastGrades(); // Ok
subClass = new PastGrades(); // Ok

🌿 대부분의 실제 코드에서 하위 클래스는 일반적으로 기본 클래스 위에 새로운 필수 타입 정보를 추가한다. 이러한 구조적 검사 동작은 예상치 못한 것처럼 보일 수 있지만 자주 발생하지는 않는다.


2. 재정의된 생성자

  • 바닐라 자바스크립트와 마찬가지로 타입스크립트에서 하위 클래스는 자체 생성자를 정의할 필요가 없다.
  • 자체 생성자가 없는 하위 클래스는 암묵적으로 기본 클래스의 생성자를 사용한다.
  • 자바스크립트에서 하위 클래스가 자체 생성자를 선언하면 super 키워드를 통해 기본 클래스 생성자를 호출해야 한다.
  • 하위 클래스 생성자는 기본 클래스에서의 필요 여부와 상관없이 모든 매개변수를 선언할 수 있다.
  • 타입스크립트의 타입 검사기는 기본 클래스 생성자를 호출할 때 올바른 매개변수를 사용하는지 확인한다.

[예제 1]
PassingAnnouncer의 생성자는 number 인수를 사용해 기본 클래스인 GradeAnnouncer의 생성자를 올바르게 호출하는 반면, FailingAnnouncer는 기본 생성자를 올바르게 호출하지 않아 타입 오류가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class GradeAnnouncer {
  message: string;

  constructor(grade: number) {
    this.message = grade >= 65 ? 'Maybe next time...' : 'You pass!';
  }
}

class PassingAnnounder extends GradeAnnouncer {
  constructor() {
    super(100);
  }
}

  • 자바스크립트 규칙에 따르면 하위 클래스의 생성자는 this 또는 super에 접근하기 전에 반드시 기본 클래스의 생성자를 호출해야 한다.
  • 타입스크립트는 super()를 호출하기 전에 this 또는 super에 접근하려고 하는 경우 타입 오류를 보고한다.

[예제 2]
ContinuedGradesTally 클래스는 super()를 호출하기 전에 생성자에서 this.grades를 잘못 참조한다.

1
2
3
4
5
6
7
8
9
class GradesTally {
  grades: number[] = [];

  addGrades(...grades: number[]) {
    this.grades.push(...grades);

    return this.grades.length;
  }
}


3. 재정의된 메서드

  • 하위 클래스의 메서드가 기본 클래스의 메서드에 할당될 수 있는 한 하위 클래스는 기본 클래스와 동일한 이름으로 새 메서드를 다시 선언할 수 있다.
  • 기본 클래스를 사용하는 모든 곳에 하위 클래스를 사용할 수 있으므로 새 메서드의 타입도 기본 메서드 대신 사용할 수 있어야 한다.

[예제]

  • FailureCounter의 countGrades 메서드는 기본 GradeCounter의 countGrades 메서드의 반환 타입과 첫 번째 매개변수와 동일하기 때문에 허용된다.
  • AnyFailureChecker의 countGrades는 잘못된 반환 타입을 가지므로 타입 오류가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
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');
  }
}


4. 재정의된 속성

  • 하위 클래스는 새 타입을 기본 클래스의 타입에 할당할 수 있는 한 동일한 이름으로 기본 클래스의 속성을 명시적으로 다시 선언할 수 있다.
  • 재정의된 메서드와 마찬가지로 하위 클래스는 기본 클래스와 구조적으로 일치해야 한다.
  • 속성을 다시 선언하는 대부분의 하위 클래스는 해당 속성을 유니언 타입의 더 구체적인 하위 집합으로 만들거나 기본 클래스 속성 타입에서 확장되는 타입으로 만든다.

[예제 1]
기본 클래스 Assignment는 grade를 number | undefined로 선언하고 하위 클래스 GradeAssignment는 grade를 항상 존재하는 number 타입으로 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Assignment {
  grade?: number;
}

class GradeAssignment extends Assignment {
  grade: number;

  constructor(grade: number) {
    super();
    this.grade = grade;
  }
}

속성의 유니언 타입의 허용된 값 집합을 확장할 수는 없으며, 만약 확장한다면 하위 클래스 속성은 더 이상 기본 클래스 속성 타입에 할당할 수 없다.

[예제 2]
VagueGrade의 value는 기본 클래스 NumbericGrade의 number 타입에 | string을 추가하려고 하므로 타입 오류가 발생한다.


🌓 추상 클래스

  • 때로는 일부 메서드의 구현을 선언하지 않고, 대신 하위 클래스가 해당 메서드를 제공할 것을 예상하고 기본 클래스를 만드는 방법이 유용할 수 있다.
  • 추상화하려는 클래스 이름과 메서드 앞에 타입스크립트의 abstract 키워드를 추가한다.
  • 이러한 추상화 메서드 선언은 추상화 기본 클래스에서 메서드의 본문을 제공하는 것을 건너뛰고, 대신 인터페이스와 동일한 방식으로 선언된다.

[예제 1]

  • School 클래스와 getStudentTypes 메서드는 abstract로 표시된다.
  • 그러므로 하위 클래스인 Preschool과 Absence는 getStudentTypes를 구현해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class School {
  readonly name: string;

  constructor(name: string) {
    this.name = name;
  }

  abstract getStudentTypes(): string[];
}

class Preschool extends School {
  getStudentTypes() {
    return ['preschooler'];
  }
}

  • 구현이 존재한다고 가정할 수 있는 일부 메서드에 대한 정의가 없기 때문에 추상 클래스를 직접 인스턴스화할 수 없다.
  • 추상 클래스가 아닌 클래스만 인스턴스화할 수 있다.

[예제 2]
School 예제에서 new School을 호출하려고 하면 타입스크립트 오류가 발생한다.

  • 추상 클래스는 클래스의 세부 사항이 채워질 거라 예상되는 프레임워크에서 자주 사용된다.
  • 클래스에서 앞서 본 school: School 예제에서처럼 값이 클래스를 준수해야 함을 나타내는 타입 에너테이션으로 사용할 수 있다.
  • 그러나 새 인스턴스를 생성하려면 하위 클래스를 사용해야 한다.


🌓 멤버 접근성

  • 자바스크립트에서는 캘래스 멤버 이름 앞에 #을 추가해 private 클래스 멤버임을 나타낸다.
  • private 클래스 멤버는 해당 클래스 인스턴스에서만 접근할 수 있다.
  • 자바스크립트 런타임은 클래스 외부 코드 영역에서 private 메서드나 속성에 접근하려고 하면 오류를 발생시킴으로써 프라이버시를 강화한다.
  • 타입스크립트의 클래스 지원은 자바스크립트의 # 프라이버시보다 먼저 만들어졌다.
  • 또한 타입스크립트는 private 클래스 멤버를 지원하지만, 타입 시스템에만 존재하는 클래스 메서드와 속성에 대해 조금 더 미묘한 프라이버시 정의 집합을 허용한다.

타입스크립트의 멤버 접근성(가시성)은 클래스 멤버의 선언 이름 앞에 다음 키워드 중 하나를 추가해 만든다.

  • public(기본값) : 모든 곳에서 누구나 접근 가능
  • protected : 클래스 내부 또는 하위 클래스에서만 접근 가능
  • private : 클래스 내부에서만 접근 가능

이러한 키워드는 순수하게 타입 시스템 내에 존재하며, 코드가 자바스크립트로 컴파일되면 다른 모든 타입 시스템 구문과 함께 키워드도 제거된다.

[예제 1]

  • Base 클래스는 두 개의 public 멤버와 한 개의 protected. 한 개의 private, 그리고 #truePrivate을 사용해 한 개의 private을 선언한다.
  • Subclass는 public과 protected 멤버는 접근할 수 있지만 private과 #truePrivate은 접근할 수 없다.
1
2
3
4
5
6
7
class Base {
  isPublicImplicit = 0;
  public isPublicExplicit = 1;
  protected isProtected = 2;
  private isPrivate = 3;
  #truePrivate = 4;
}

  • 타입스크립트의 멤버 접근성은 타입 시스템에서만 존재하는 반면 자바스크립트의 private 선언은 런타임에도 존재한다는 점이 주요 차이점이다.
  • protected 또는 private으로 선언된 타입스크립트 클래스 멤버는 명시적으로 또는 암묵적으로 public으로 선언된 것처럼 동일한 자바스크립트 코드로 컴파일된다.
  • 인터페이스와 타입 애너테이션처럼 접근성 키워드는 자바스크립트로 컴파일될 때 제거된다.
  • 자바스크립트 런타임에서는 # private 필드만 진정한 private이다.
  • 접근성 제한자는 readonly와 함께 표시할 수 있다.
  • readonly와 명시적 접근성 키워드로 멤버를 선언하려면 접근성 키워드를 먼저 적어야 한다.

[예제 2]
TwoKeywords 클래스는 name 멤버를 private과 readonly로 선언한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class TwoKeywords {
  private readonly name: string;

  constructor() {
    this.name = 'Anne Sullivan'; // Ok
  }

  log() {
    console.log(this.name); // Ok
  }
}

const two = new TwoKeywords();

  • 타입스크립트의 이전 멤버 접근성 키워드를 자바스크립트의 # private 필드와 함께 사용할 수 없다는 점을 기억해야 한다.
  • private 필드는 기본적으로 항상 private이므로 private 키워드를 추가로 표시할 필요가 없다.


정적 필드 제한자

  • 자바스크립트는 static 키워드를 사용해 클래스 자체에서 멤버를 선언한다.
  • 타입스크립트는 static 키워드를 단독으로 사용하거나 readonly와 접근성 키워드를 함께 사용할 수 있도록 지원한다.
  • 함께 사용할 경우 접근성 키워드를 먼저 작성하고, 그 다음 static, readonly 키워드가 온다.

[예제]
Question 클래슨느 protected, static, readonly를 모두 사용해 prompt와 answer 속성을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Question {
  protected static readonly answer: 'bash';
  protected static readonly prompt =
    "What's an ogre's favorite programming language?";

  guess(getAnswer: (prompt: string) => string) {
    const answer = getAnswer(Question.prompt);

    // Ok
    if (answer === Question.answer) {
      console.log('You got it!');
    } else {
      console.log('Try again...');
    }
  }
}

static 클래스 필드에 대해 readonly와 접근성 제한자를 사용하면 해당 필드가 해당 클래스 외부에서 접근되거나 수정되는 것을 제한하는 데 유용하다.


Reference

러닝 타입스크립트 (Learning TypeScript)

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[LearningTS] Chapter07. 인터페이스

[LearningTS] Chapter09. 타입 제한자