Contents

타입스크립트 핸드북 6장 - 클래스

https://typescript-kr.github.io/pages/classes.html

  • 컴포넌트의 재사용: 생산성을 높이기 위한 매우 중요하고 유용한 방법
  • ES6 이전 JS에서는 이를 위해 함수 프로토타입 기반 상속을 사용
  • ES6 이후 JS에서는 객체 지향적 클래스 기반의 접근 방식을 사용 가능
  • TS는 JS 버전과 무관히 클래스 기반의 접근 기법을 사용할 수 있게 함

클래스

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
  • 클래스 내부에서 클래스 멤버 참조 시 this 를 통해 참조 가능
  • new를 사용해 클래스를 인스턴스화 한다.
  • 사전 정의한 생성자를 호출해
    • 클래스 형태의 새로운 객체를 생성하고
    • 생성자를 실행해 생성된 객체를 초기화한다.

클래스의 상속

상속은 클래스 기반 프로그래밍의 가장 기본적인 패턴이다.

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();
  • Animal은 기반이 되는, 슈퍼 클래스이다.
  • Animal로부터 파생된 Dog 클래스는 서브 클래스이다.
  • 서브 클래스의 생성자 함수는 슈퍼 클래스의 생성자를 super()를 통해 호출해야 한다.
    • 이 때 생성자의 this 를 통해 프로퍼티에 접근하기 전에 super()를 먼저 호출해야 함에 유의해야 한다.
    • https://stackoverflow.com/questions/60689986/why-super-should-come-before-accessing-this
    • 서브 클래스에서 슈퍼 클래스의 초기화되지 않은 멤버를 참조하는 것을 막기 위함
    • 슈퍼 클래스는 서브 클래스의 존재를 몰라야 한다.
      • 서브 클래스에서 프로퍼티 설정 후 super() 호출 시 JS의 작동 방식 때문에 슈퍼 클래스 생성자가 서브 클래스의 프로퍼티에 접근 가능하다.

접근 지정자

TS의 클래스는 기본적으로 public으로 지정된다.

ECMAScript 비공개 필드

class Animal {
    #name: string;
    constructor(theName: string) { this.#name = theName; }
}

new Animal("Cat").#name; // 프로퍼티 '#name'은 비공개 식별자이기 때문에 'Animal' 클래스 외부에선 접근할 수 없습니다.
  • JS 런타임에 내장되어 있는 문법
  • 각각의 비공개 필드의 isolation을 잘 보장할 수 있는 방법

private 지정자

private 지정자를 통해 클래스 외부에서 멤버에 접근 불가능하도록 설정할 수 있다.

  • TS는 구조적인 타입 시스템
    • 두 타입을 비교할 때 모든 멤버의 타입이 호환된다면 서로 호환가능한 타입이다.
  • 하지만 이는 private 또는 protected 접근 지정자가 붙은 타입 비교시 다르게 처리된다.
    • 호환된다고 판단되는 두 타입 중 하나가 private 멤버를 가지고 있다고 가정
    • 다른 한 쪽도 무조건 동일하게 private 멤버를 가지고 있어야 한다.
    • protected도 마찬가지

protected 지정자

protected 지정자는 protected로 선언된 멤버를 파생된 서브 클래스 내에서 접근 가능하다는 점을 빼곤 private 지정자와 거의 똑같다.

생성자가 protected로 지정될 수 있음이 특이했다.

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee는 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"); // 오류: 'Person'의 생성자는 protected 입니다
  • 클래스를 포함하는 클래스 외부에서 인스턴스화는 불가능 (new를 사용한`)
  • 클래스 외부에서 확장은 가능하다. (extends를 통한)

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"; // 오류! name은 읽기전용 입니다.

위 예시는 다음과 같이 프로퍼티의 선언과 초기화를 동시에 할 수 있다.

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}
  • 매개변수 프로퍼티는 접근 지정자 또는 readonly 또는 둘 모두를 생성자 매개변수에 prefix로 붙여서 선언 가능
  • 매개변수 프로퍼티에 private를 붙이면 private 멤버를 선언과 동시에 초기화
  • 마찬가지로 public, protected, readonly도 동일하게 동작함

접근자

TS는 각 객체의 멤버에 대한 접근자를 지원한다.

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);
}
  • TS가 기본적으로 지원하는 getter / setter를 통해서 입력에 대한 validation을 수행할 수 있다.
  • 접근자는 ES5 이상을 출력하도록 컴파일 옵션을 설정해야 함에 유의
  • get 또는 set이 없는 접근자는 자동으로 readonly로 유추된다.
    • 프로퍼티 내 사용자들이 변경할 수 없음을 알게 해주기 때문에 .d.ts 파일 생성시 유용

전역 프로퍼티

TS는 static 지정자를 통해 인스턴스가 아닌 클래스 자체에서 보이는 전역 멤버 생성 가능

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}));

추상 클래스

  • 추상 클래스는 다른 클래스들이 파생될 수 있는 기초 클래스
  • 직접 인스턴스화 불가능
  • 인터페이스와는 달리 멤버에 대한 세부 구현 정보 포함 가능
  • abstract 키워드를 통해 추상 클래스를 선언하고, 추상 메서드를 정의하는데 사용가능
abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log("Department name: " + this.name);
    }

    abstract printMeeting(): void; // 반드시 파생된 클래스에서 구현되어야 합니다.
}

class AccountingDepartment extends Department {

    constructor() {
        super("Accounting and Auditing"); // 파생된 클래스의 생성자는 반드시 super()를 호출해야 합니다.
    }

    printMeeting(): void {
        console.log("The Accounting Department meets each Monday at 10am.");
    }

    generateReports(): void {
        console.log("Generating accounting reports...");
    }
}

let department: Department; // 추상 타입의 레퍼런스를 생성합니다
department = new Department(); // 오류: 추상 클래스는 인스턴스화 할 수 없습니다
department = new AccountingDepartment(); // 추상이 아닌 하위 클래스를 생성하고 할당합니다
department.printName();
department.printMeeting();
department.generateReports(); // 오류: 선언된 추상 타입에 메서드가 존재하지 않습니다

인터페이스 vs 추상 클래스의 차이점

  • 인터페이스는 프로퍼티들과 인터페이스를 구현하는 객체가 할 수 있는 행동을 사전에 정의하는 것
    • 인터페이스를 구현하는 것은 자유이다. 때문에 런타임에 존재하지 않는다. (instanceof로도 당연히 검사 불가능하다)
  • 추상 클래스는 온전하지 못한 구현체이다.
    • 추상 클래스는 (추상 메서드만 존재하더라도) 런타임에 존재한다.

생성자 함수

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 = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

let greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"

위 타입스크립트 코드를 조금 수정해보면 재밌는 것을 알 수 있는데,

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return 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!"

greeterMaker 변수는 Greeter 라는 심볼의 타입을 제공한다.

  • 해당 타입은 Greeter 클래스 인스턴스를 만드는 생성자를 포함한다.
  • 아울러 Greeter의 모든 static 멤버를 포함한다. (때문에 static 멤버는 생성자 없이 호출 가능하다)
  • new를 사용한다면 새로운 인스턴스를 호출할 수 있다.