객체 지향 접근 방식에 익숙한 입장에서는 클래스가 함수를 상속받고 이런 클래스에서 객체가 만들어지는 것에 다소 어색함을 느낄 수 있다. JavaScript 프로그래머들은 이런 객체 지향적 클래스 기반의 접근 방식을 사용해서 프로그램을 작성할 수 있다.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
새로운 클래스 Greeter
는 3개의 멤버를 가지고 있다.
greeting
프로퍼티greet()
method클래스 안에서 클래스의 멤버를 참조할 때는 this
를 사용한다.이것은 멤버에 접근하는 것을 의미한다.
마지막 줄에서, new
를 사용하여 Greeter
클래스의 인스턴스를 생성한다. 이 코드는 이전에 정의한 생성자를 호출하여 Greeter
형태의 새로운 객체를 만들고, 생성자를 실행해 초기화한다.
TypeScript에서는, 일반적인 객체 지향 패턴을 사용할 수 있다. 클래스 기반 프로그래밍의 가장 기본적인 패턴 중 하나는 상속을 이용하여 이미 존재하는 클래스를 확장해 새로운 클래스를 만들 수 있다는 것이다.
class Animal {
move(distance: number = 0) {
console.log(`Animal moved ${distance}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('bark.');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
클래스는 기초 클래스로부터 프로퍼티와 method를 상속받는다. Dog
은extends
키워드를 사용하여 Animal
이라는 기초 클래스로부터 파생된 파생 클래스이다. 파생된 클래스는 하위 클래스(subclasses), 기초 클래스는 상위 클래스(superclasses)로 불린다.
extends
예제class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
moved(distance = 5) {
console.log("Slithering...");
super.move(distance);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distance = 45) {
console.log("Galloping...");
super.move(distance);
}
}
let sam = new Snake("Sammy the Python");
let tom = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
Animal
을 extends
해서 하위 클래스 Horse
와 Snake
를 생성한다. 위와 같이 클래스를 상속 받을 때 주의할 점이 있다.
super()
를 실행해야 한다.this
에 있는 프로퍼티에 접근하기 전에 super()
를 호출해야 한다.이 점은 TypeScript에서 중요한 규칙이다.
TypeScript에는 멤버를 포함하는 클래스 외부에서 이 멤버에 접근하지 못하도록 멤버를 private
으로 표시하는 방법이 있다.
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error. 'name'은 private로 선언되어 있다.
private
및 protected
멤버가 있는 타입들을 비교할 때는 타입을 다르게 처리한다. 호환된다고 판단되는 두 개의 타입 중 한 쪽에서 private
멤버를 가지고 있다면, 다른 한 쪽도 무조건 동일한 선언에 private
멤버를 가지고 있어야 한다. 이것은 protected
멤버에도 적용된다.
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino; // 호환 가능
animal = employee; // Error. 'Animal'과 'Employee'는 호환 불가능
위 예제에서 Animal
과 그를 상속 받은 클래스 Rhino
가 있다. Animal
과 형태가 같아보이는 Employee
라는 클래스도 있다. Animal
과 Rhino
는 Animal
의 private name: string
이라는 동일한 선언으로부터 private
부분을 공유하기 때문에 호환이 가능하다. 하지만 Employee
의 경우 name
이라는 private
멤버를 보유하지만, Animal
에서 선언한 것이 아니기 때문이다.
protected
지정자도 protected
멤버를 파생된 클래스 내에서 접근할 수 있다는 점만 제외하면 pirvate
지정자와 매우 유사하게 동작한다.
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `name: ${this.name}, work: ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // Error
Person
외부에서 name
을 사용할 수 없지만, Employee
는 Person
에서 파생되었기 때문에 Employee
의 인스턴스 method 내에서는 여전히 사용할 수 있다. 생성자 또한 protected
로 표시될 수 있다.
readonly
키워드를 사용하여 프로퍼티를 읽기전용으로 만들 수 있다. 읽기전용 프로퍼티들은 선언 또는 생성자에서 초기화해야 한다.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let add = new Octpus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit";
// Error. name은 readonly.
위의 Octopus
클래스의 생성자 매개변수를 좀 더 간단하게 선언할 수 있다.
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
생성자에 짧아진 readonly name: string
파라미터를 사용하여 theName
을 제거하고 name
멤버를 생성하고 초기화했다. 즉 선언과 할당을 한 곳으로 통합했다.
매개변수 프로퍼티에 private
를 사용하면 비공개 멤버를 선언하고 초기화한다. 마찬가지로, public
, protected
, readonly
도 동일하게 작용한다.
TypeScript는 객체의 멤버에 대한 접근 방식으로 getters/setters를 지원한다.
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if(employee.fullName) {
console.log(employee.fullName);
}
위는 getters/setters가 없는 예제이다. 임의로 fullName
을 직접 설정할 수 있도록 허용하는 것은 매우 편리하지만, 경우에 따라 fullName
이 설정될 때 몇 가지 제약 조건을 걸어야 할 수 도 있다.
const fullNameMaxLength = 10;
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if(newName && newName.length > fullNameMaxLength) {
throw new Error("fullName has a max length of " + fullNameMaxLength);
}
this._fullName = newName;
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if(employee.fullName) {
console.log(employee.fullName);
}
위 예시에서는 newName
의 길이가 상수 fullNameMaxLength
의 길이를 넘지 않는지 확인하는 setter
를 추가한다. 만약 최대 길이를 초과한다면 new Error
로 오류를 발생시킨다. 기존의 기능을 유지하기 위해 fullName
을 수정하지 않는 getter
도 생성한다.
추상 클래스는 다른 클래스들이 파생될 수 있는 기초 클래스이다. 추상 클래스는 직접 인스턴스화 할 수 없다. abstract
키워드는 추상 클래스 뿐만 아니라 추상 클래스 내에서 추상 method를 정의하는데 사용된다.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
추상 클래스 내에서 추상으로 표시된 method는 구현을 포함하지 않으며 반드시 파생된 클래스에서 구현되어야 한다. 추상 method는 인터페이스 method와 비슷한 문법을 공유하나, 추상 method는 반드시 abstract
키워드를 포함해야 한다.
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void;
// 반드시 파생된 클래스에서 구현되어야 하는 method
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing");
// 파생된 클래스의 생성자는 반드시 super()를 호출해야 한다.
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // 추상 타입의 레퍼런스 생성
department = new Department(); // Error. 추상 클래스는 인스턴스화 불가능
department = new AccountingDepartment(); // 추상이 아닌 하위 클래스를 생성하고 할당
department.printName();
department.printMeeting();
department.generateReports(); // Error. 선언된 추상 타입에 method가 존재하지 않음
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };
클래스 선언은 클래스의 인스턴스를 나타내는 타입과 생성자 함수라는 두 가지를 생성한다. 클래스는 타입을 생성하기 때문에 인터페이스를 사용할 수 있는 동일한 위치에서 사용 가능하다.