✔️

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

조금 더 복잡한 예제로 살펴보기

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

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;
🚨
Property '#name' is not accessible outside class 'Animal' because it has a private identifier.

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;
🚨
Property 'name' is private and only accessible within class 'Animal'.

아래 예제에서 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;
🚨
Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.

각 클래스의 인스턴스를 만들고 서로에게 할당해서 어떤 일이 일어나는지 살펴봄

// 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);
🚨
Property 'name' is protected and only accessible within class 'Person' and its subclasses.

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");
🚨
Constructor of class 'Person' is protected and only accessible within the class declaration.

해당 클래스를 상속하는 클래스 바깥에서는 초기화될 수 없지만 실행할 수는 있음

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";
🚨
Cannot assign to 'name' because it is a read-only property.

Parameter properties

파라미터 프로퍼티를 사용하면 한 곳에서 멤버를 만들고 초기화할 수 있음

바로 위에서 만든 예제를 다음과 같이 수정할 수 있음

class Octopus {
  readonly numberOfLegs: number = 8;
  constructor(readonly name: string) {}
}

let dad = new Octopus("Man with the 8 strong legs");
dad.name;

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

주의사항

  1. Getter/Setter는 ECMAScript 5 이상을 사용하도록 컴파일러를 설정해야 함
  1. get과 set이 없는 프로퍼티는 자동으로 읽기 전용으로 추정됨

    → 코드에서 .d.ts 파일을 생성할 때 유용함

    왜냐하면 프로퍼티 사용자가 그것을 변경할 수 없다는 것을 알 수 있기 때문

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로 하는 것이..

고오급 기술

생성자 함수

타입스크립트에서 클래스를 선언하면 동시에 여러 가지가 만들어짐

클래스의 인스턴스 타입

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"

→ 클래스에는 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!"

클래스를 인터페이스로 사용하기

위에서 말한대로 클래스의 선언은 두 가지를 만들어냄

클래스가 타입을 만들기 때문에, 인터페이스에서도 사용할 수 있음

class Point {
  x: number;
  y: number;
}

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };