06. Class
자바스크립트는 함수와 prototype 기반의 상속을 이용해서 재사용 가능한 컴포넌트를 만듦
그러나 이런 방법은 클래스 방식의 객체지향 접근법에 익숙한 프로그래머에게는 다소 이상하게 느껴질 수 있음
ES6부터는 자바스크립트에서도 class 기반의 객체지향 접근법을 사용할 수 있게 됨
타입스크립트에서는 새 버전의 자바스크립트를 사용하지 않아도
이러한 문법을 지원함과 동시에 주요 브라우저와 플랫폼에 사용 가능한 형태로 컴파일해줌
Classes
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
// target: es5
var Greeter = /** @class */ (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
}());
var greeter = new Greeter("world");
// target: es6
class Greeter {
constructor(message) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
Inheritance
아주 기초적인 예제로 살펴보기
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log("Woof! Woof!");
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
target: es5
로 컴파일된 버전 보기var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Animal = /** @class */ (function () { function Animal() { } Animal.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 0; } console.log("Animal moved " + distanceInMeters + "m."); }; return Animal; }()); var Dog = /** @class */ (function (_super) { __extends(Dog, _super); function Dog() { return _super !== null && _super.apply(this, arguments) || this; } Dog.prototype.bark = function () { console.log("Woof! Woof!"); }; return Dog; }(Animal)); var dog = new Dog(); dog.bark(); dog.move(10); dog.bark();
extends
키워드를 사용해서 다른 클래스를 상속할 수 있음- 추억 돋는 C++ 상속 방법 보기 - https://blog.hexabrain.net/173
- 클래스는 base 클래스의 프로퍼티와 메서드를 상속함
=
Animal
을 상속한Dog
는 bark와 move 메서드를 둘 다 가짐
- 상속 받은 클래스 = subclass, 상속 하는 클래스 = superclass 라고 부름
조금 더 복잡한 예제로 살펴보기
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
target: es5
로 컴파일된 버전 보기var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var Animal = /** @class */ (function () { function Animal(theName) { this.name = theName; } Animal.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 0; } console.log(this.name + " moved " + distanceInMeters + "m."); }; return Animal; }()); var Snake = /** @class */ (function (_super) { __extends(Snake, _super); function Snake(name) { return _super.call(this, name) || this; } Snake.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 5; } console.log("Slithering..."); _super.prototype.move.call(this, distanceInMeters); }; return Snake; }(Animal)); var Horse = /** @class */ (function (_super) { __extends(Horse, _super); function Horse(name) { return _super.call(this, name) || this; } Horse.prototype.move = function (distanceInMeters) { if (distanceInMeters === void 0) { distanceInMeters = 45; } console.log("Galloping..."); _super.prototype.move.call(this, distanceInMeters); }; return Horse; }(Animal)); var sam = new Snake("Sammy the Python"); var tom = new Horse("Tommy the Palomino"); sam.move(); tom.move(34);
super()
: base 클래스의 생성자를 호출하는 함수 (타입스크립트가 강제하는 rule)상속받은 클래스의 생성자에서
super()
를 호출하지 않으면 오류가 발생함class Character { constructor(){ console.log('invoke character'); } } class Hero extends Character{ constructor(){ super(); // exception thrown here when not called console.log('invoke hero'); } } var hero = new Hero();
좀 더 찾아보니까 이는 타입스크립트에서 강제하는 룰이 아니라, 자바스크립트의 규칙을 따라가는 것
- In a child class constructor,
this
cannot be used untilsuper
is called.
- ES6 class constructors MUST call
super
if they are subclasses, or they must explicitly return some object to take the place of the one that was not initialized.
- In a child class constructor,
- subclass에서 superclass의 메서드를 오버라이드(재정의) 할 수 있음
# 실행 결과물 Slithering... Sammy the Python moved 5m. → Snake에서 재정의 됨 Galloping... Tommy the Palomino moved 34m. → Horse에서 재정의 됨
Public, private, and protected modifiers
public (by default)
일반적으로 다른 프로그래밍 언어에서는 superclass의 속성에 접근하려면
public
키워드를 명시해줘야하는데, 위 예제에서는 그런 키워드 없이 접근하고 있는 것을 알 수 있음
= 각 멤버들은 public
을 기본 상태로 가짐
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
이렇게 public
키워드를 명시해도 됨
ECMAScript Private Fields
typescript 3.8 에서는 #
prefix를 붙여서 네이밍하면 private 식별자가 됨
class Animal {
#name: string;
constructor(theName: string) {
this.#name = theName;
}
}
new Animal("Cat").#name;
target: es6
로 컴파일된 버전 보기var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; }; var _name; class Animal { constructor(theName) { _name.set(this, void 0); __classPrivateFieldSet(this, _name, theName); } } _name = new WeakMap();

target: es5
일 때 어떻게 변환하는지 궁금해서 써봤더니 이런 오류가 뜸.. = 못씀
왜 안되냐면 그것은 ES6에 새로 등장한 특성(정확히는 stage 3 proposal)이기 때문..
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields
class ClassWithPrivateField {
#privateField
}
class ClassWithPrivateMethod {
#privateMethod() {
return 'hello world'
}
}
class ClassWithPrivateStaticField {
static #PRIVATE_STATIC_FIELD
}
private
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
new Animal("Cat").name;
private
키워드를 명시해서 멤버를 private로 만들 수 있는데,이 경우 클래스 외부에서 해당 멤버에 접근할 수 없음
- 타입스크립트는 구조형(structural) 타입 시스템으로 두 가지 타입을 비교할 때
각각이 어디에서 왔는지와 관계 없이 모든 멤버의 타입이 호환 가능하면 두 타입이 호환 가능하다고 봄
private
,protected
멤버가 있는 타입을 비교하는 경우 이런 타입을 다르게 처리함두 타입이 호환 가능하려면
- 하나가
private
이라면, 다른 멤버도 동일한 선언에서 비롯한private
을 가져야 함
protected
에 대해서도 위와 동일한 rule이 적용됨
- 하나가
아래 예제에서 Animal과 Employee의 name이 public 이면 두 타입은 호환 가능함
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;
Animal
과Rhino
클래스가 있고Rhino
는Animal
의 subclass
Animal
과 형태가 동일한Employee
클래스도 있음
각 클래스의 인스턴스를 만들고 서로에게 할당해서 어떤 일이 일어나는지 살펴봄
Animal
과Rhino
는Animal
의private name: string
을 공유하기 때문에 호환 가능함
Employee
에 선언된private name: string
은Animal
에 선언된 것이 아니기 때문에 호환 불가능함
// target: es6로 컴파일된 버전
class Animal {
constructor(theName) {
this.name = theName;
}
}
class Rhino extends Animal {
constructor() {
super("Rhino");
}
}
class Employee {
constructor(theName) {
this.name = theName;
}
}
protected
protected로 명시된 멤버는 상속된 클래스 내에서 사용할 수 있음
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 `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name);
Person
의 protected인name
은 상속된Employee
내에서 접근(getElevatorPitch
) 할 수 있지만바깥에서는(마지막 줄) 접근할 수 없음
protected 생성자
생성자도 protected로 표시될 수 있음
class Person {
protected name: string;
protected constructor(theName: string) {
this.name = theName;
}
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John");
해당 클래스를 상속하는 클래스 바깥에서는 초기화될 수 없지만 실행할 수는 있음
Readonly modifier
readonly
키워드를 이용해서 프로퍼티를 읽기 전용 속성으로 만들 수 있음
readonly
프로퍼티는 선언될 때 혹은 생성자에서 반드시 초기화되어야 함
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit";
Parameter properties
파라미터 프로퍼티를 사용하면 한 곳에서 멤버를 만들고 초기화할 수 있음
바로 위에서 만든 예제를 다음과 같이 수정할 수 있음
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name;
- name 속성의 선언과 할당이 생성자의 파라미터 위치에서 동시에 이루어짐
- 접근자(Access Modifier),
readonly
중 하나를 접두어로 붙이면 파라미터 프로퍼티를 선언할 수 있음- 접근자 = protected, private, public
Accessors - 접근자
타입스크립트에는 객체의 멤버에 대한 액세스를 지원하는 Getter/Setter 메서드가 있음
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
get/set이 없는 심플한 클래스를 접근자를 가지도록 변경해볼 것
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);
}
- setter에서 정해진 규칙(여기에서는 10자 미만)을 지키지 않으면
값을 변경하지 않고 오류를 발생하는 방법으로 값을 더 유효하게 관리할 수 있음
- 보통 다른 언어에서는 저런 setter가 정해진게 아니라
setPropertyName
이름의 메서드를 만들고해당 메서드를 호출하는 방식인데 여기에서는 값 할당은 기존처럼 하면 되니 조금 낯설음..ㅎ
주의사항
- Getter/Setter는 ECMAScript 5 이상을 사용하도록 컴파일러를 설정해야 함
- get과 set이 없는 프로퍼티는 자동으로 읽기 전용으로 추정됨
왜냐하면 프로퍼티 사용자가 그것을 변경할 수 없다는 것을 알 수 있기 때문
Static Properties
클래스 인스턴스가 아니라 클래스 자체에서 볼 수 있는 클래스의 정적 멤버를 생성 가능
class Grid {
static origin = { x: 0, y: 0 };
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(public scale: number) {}
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));
인스턴스 멤버에 접근할 때 this.
를 사용했다면 static 멤버는 ClassName.
으로 접근 가능
Abstract Class
추상 클래스는 다른 클래스를 파생시킬 수 있는 기본 클래스이지만, 직접 인스턴스화 할 수 없음
인터페이스와 다른 점은 멤버에 대한 구현 세부 정보를 포함할 수 있음
abstract
키워드는 추상 클래스 내의 추상 메서드 뿐 아니라 추상 클래스를 정의할 때도 사용됨
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
abstract로 표시된 추상 클래스 내의 메서드는 구현이 없기 때문에
상속하는 subclass에서 직접 구현해야 함

어떻게 컴파일되는지 살펴봤는데, 추상 메서드는 그냥 없어짐
= 정적 레벨에서 추상 클래스 상속했는데 구현 안하면 린트 오류만 띄워줌
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // must be implemented in derived classes
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // constructors in derived classes must call super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: department is not of type AccountingDepartment, cannot access generateReports
// 꼭 호출하고 싶다면
(department as AccountingDepartment).generateReports();
// 그러나 사실 department 타입 자체를 AccountingDepartment로 하는 것이..
- 추상 메서드는 인터페이스와 유사한 메서드 구문을 사용함
둘 다 메서드 본문을 구현하지 않고, Method Signature를 정의함
- 추상 메서드는 인터페이스와 다르게
abstract
키워드를 포함해야 하고,선택적으로 Getter/Setter를 포함할 수 있음
고오급 기술
생성자 함수
타입스크립트에서 클래스를 선언하면 동시에 여러 가지가 만들어짐
클래스의 인스턴스 타입
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"
let greeter: Greeter
: Greeter
를 클래스 인스턴스의 타입으로 사용함
다른 객체지향 언어를 사용하는 프로그래머들에게 아주 자연스러운 행동
생성자 함수 - constructor function
new
키워드를 이용해서 클래스의 인스턴스를 생성할 때 호출하는 함수
위의 코드를 자바스크립트로 변환한 결과물을 살펴보면
var Greeter = /** @class */ (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
}());
var greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"
new
키워드와 함께 생성자 함수를 호출하면 클래스 인스턴스가 생성됨
- 생성자 함수는 클래스의 모든 정적 멤버를 가지고 있음
→ 클래스에는 instance side와 static side가 있음 (지난주에 봤던 그 글)
위의 예제를 조금 고쳐봅시다
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
} else {
return Greeter.standardGreeting;
}
}
}
Greeter.standardGreeting
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet()); // "Hello, there"
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // "Hey there!"
let greeter3: Greeter;
greeter3 = new Greeter();
console.log(greeter3.greet()); // "Hey there!"
greeter1
: 이전 예제의greeter
와 유사함
greeterMarker
:Greeter
클래스를 직접적으로 사용함, 클래스 자체(= 생성자 함수)를 변수에 가짐typeof Greeter
: “give me the type of theGreeter
class itself” 또는“give me the type of the symbol called
Greeter
,”의 의미
클래스를 인터페이스로 사용하기
위에서 말한대로 클래스의 선언은 두 가지를 만들어냄
- 인스턴스를 나타내는 타입
- 생성자 함수
클래스가 타입을 만들기 때문에, 인터페이스에서도 사용할 수 있음
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };