6. typescript - class

2020. 3. 6. 15:33typescript/typescript-grammar

예전 부터 js에서는 함수 그리고 prototype-based를 사용한 상속으로 재사용가능한 컴포넌트들을 만들었습니다. 하지만 클래스가 기능을 상속하고 이러한 클래스로 부터 객체를 만드는 과정이 프로그래머가 편하게 객체지향으로 접근하기 어색합니다. ECMA-SCRIPT 6가 사용가능해지면서 클래스 기반의 접근이 가능해졌습니다. 타입스크립트에서 우리는 클래스를 사용할 수 있고 자바스크립트로 컴파일 하여 모든 주요 브라우저, 플랫폼에서 사용할 수 있습니다. 

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello ${this.greeting}`;
  }
}

let greeting = new Greeter("TS");

 

다른 언어 C#이나 java와 비슷합니다. 여기서는 새로운 class Greeter를 만들었습니다. 이 클래스에서는 세개의 멤버를 갖고 있습니다. greeting, constructor, greet 입니다. 

 

그리고 클래스 내부에서 member에 접근하기 위해 this라는 용어를 사용합니다. 그리고 마지막 줄에 자바와 비슷하게 new class이름(constructor에 들어가는 값)를 작성하여 클래스로 부터 새로운 객체를 만들 수 있습니다. 

상속

타입스크립트에서 우리는 객체지향 패턴을 사용할 수 있습니다. 가장 기초적인 패턴 중 하나는 클래스 기반의 프로그래밍 입니다.  그리고 이는 존재하는 클래스를 연장하여 새로운 클래스를 만들 수 있습니다.

 

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("woof");
  }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

위 예제에서는 가장 간단한 상속의 예제를 보여줍니다. 클래스는 base가 되는 클래스에게 properties와 method를 상속 받는다. 여기서 Dog는 Animal로 부터 파생된 클래스이다. base class는 extends라는 키워드로 상속을 이용할 수 있다. 파생된 클래스는 흔히 sub class라고 부르고 base 클래스는 super class라고 부른다.

 

왜냐하면 dog는 animal을 기능적으로 확장한 것입니다. 그래서 dog의 인스턴스를 만들어 주면 bark(), move() 두가지 기능을 활용할 수 있습니다. 

class Snake extends Animal {
  constructor(name) {
    super(name);
  }
  move(distanceInMeters = 5) {
    console.log("Slithering");
    super.move(distanceInMeters);
  }
}

class Horse extends Animal {
  constructor(name) {
    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);

 

위 예제는 이전 예제와 다른 상속의 특성을 보여주고 있습니다. 우선 super()입니다. super를 사용하면 base 클래스의 constructor 및 method를 사용할 수 있습니다. 또한 오버라이딩을 통해 move를 구현해주고 있습니다. 그래서 tom은 Animal 타입을 지정하고 있지만 new Horse를 통해 인스턴스를 생성해주면 Galloping이 나오는 것입니다. 

 

private, public, protected

위의 예제들에서 우리는 자유롭게 멤버들에게 접근할 수 있습니다. 우리는 다른 언어와 같이 public등의 명시 없이 위의 예제를 작성한 것을 알 수 있다. 그렇다. 타입스크립트에서는 default로 설정된 접근자가 public이다. 

 

class Animal {
  public name: string;
  public constructor(name: string) {
    this.name = name;
  }
  public move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

 

타입스크립트 3.8에서는 private field를 위한 새로운 자바스크립트의 구문을 지원한다. 

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.

 

이 구문은 자바스크립트 런타임에 만들어지고 각 private field를 분리하여 증명하기 쉽다. https://devblogs.microsoft.com/typescript/announcing-typescript-3-8-beta/#ecmascript-private-fields

 

Announcing TypeScript 3.8 Beta | TypeScript

Today we’re announcing the availability of TypeScript 3.8 Beta! This Beta release contains all the new features you should expect from TypeScript 3.8’s final release. To get started using the beta, you can get it through NuGet, or through npm with the foll

devblogs.microsoft.com

 

위 방법 말고도 당연히 private 구문을 사용하여 private를 선언할 수 있습니다. 

class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

const animal = new Animal("cat");
animal.name;

 

만약 member 들중 하나가 private라면 그 member는 같은 선언이 된 곳에서 무조건 사용해야한다. 

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' and 'Employee' are not compatible

위 예제에서는 Rhino와 Employee, Animal이 있고 Rhino는 Animal의 subclass이다. 그리 Employee는 Animal과 동일한 모양을 갖고 있습니다. 하지만 에러가 발생하는 모습을 볼 수 있습니다. 상속하지 않으면 아무리 모습이 똑같아도 객체를 만들 수 없음을 뜻합니다. 

 

protected

 

protected는 마치 private와 같이 행동한다. 또한 파생된 클래스에서 접근할 수 있다. 

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

 

마지막 줄은 Person의 밖에서 name을 사용하려다가 에러가 났다. 그러나 Employee는 Person의 파생된 클래스이기 때문에 사용할 수 있다. 

 

또한 생성자에게도 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"); // Error: The 'Person' constructor is protected

 

ReadOnly 

인터페이스와 같이 class에서도 property에게 readonly를 사용하여 값을 변경할 수 없게 만들 수 있다. 

class Octopus {
  readonly name: string;
  readonly legs: number = 8;

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

let dad = new Octopus("I have Third Strong leg");
dad.name = "change Error";

 

parameter properties

parameter properties는 한곳에서 선언되고 초기화 된다. 아래의 좀 더 개선된 클래스를 보여주면 파라미터 property를 갖고 있습니다. 

class Octopus {
  readonly legs: number = 8;

  constructor(readonly name: string) {}
}

let dad = new Octopus("I have Third Strong leg");
dad.name;

 

첫 예제와 다르게 어떻게 쉽게 사용하고 줄였는지 확인할 수 있습니다. 우리는 이를 통해 한장소에서 선언과 할당이 통합된 것을 볼 수 있다. 모든 멤버들은 선언과 초기화를 해주어야 한다. public, private, protected, readonly 모두 

 

Accessors

오브젝트의 멤버에게 접근하는 방법 중 하나는 setter와 getter를 이용하는 것이다. 이는 각 오브젝트에 멤버들에게 더 세분화하여 접근하고 컨트롤할 수 있게 해준다. 

 

class Employee {
  fullName: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
  console.log(employee.fullName);// 출력 완료
}

 

위 예제를 개선된 코드로 member에 바로 접근하는 것이 아닌 set과 get을 이용하여 변경 및 값을 가져 오겠습니다. 

class Employee {
  _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    this._fullName = newName;
  }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
  console.log(employee.fullName); // 출력 완료
}

우선 typescript에서 지원 하는 get, set을 사용하여 fullname을 변경 및 가져오고 있습니다. 이 기능을 사용하려면 EcmaScript5 이상이여야 합니다. get과 set은 자동으로 readonly로 추론됩니다. 이는 다른 사람들이 당신의 property를 변경할 수 없게 하기 위해서입니다. 

 

Static Properties

클래스안에서 static이라는 구문을 사용하여 만들 수 있다. static member는 인스턴스를 통해 접근하기 보다는 클래스 스스로 접근할 수 있다. 앞에 this를 붙이는 것과 같이 각 인스턴스는 클래스를 연장하여 접근할 수 있다. 

class Grid {
  static origin: { x: number; y: number } = { 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 Class 

abstract Class 파생된 다른 클래스의 base Class이다. abstract class는 interface와는 다르게 몇몇의 멤버들이 구현되어있다. abstract라는 구문은 abstract class안에 들어있는 몇몇의 메소드에 붙기도 한다. 

abstract class Animal2 {
  abstract makeSound(): void;
  move(): void {
    console.log("roaming the earth...");
  }
}

 

abstract class안에 들어있는 메소드들은 구현되지 않은채 포함되어있다. 이는 반드시 파생된 class에 구현해주어야한다. 물론 구현되지 않은 method들도 abstract keyword를 붙여주어야 한다. 

 

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: method doesn't exist on declared abstract type

위 예제코드를 보면 abstract class는 instance를 만들 수 없다. 그리고 department의 타입이 Department이기 때문에 Department에 정의된 method만 사용할 수 있다. 그래서 자식 클래스에서 abstract로 구현된 함수를 사용할 수 있다. 

 

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!"

 

 

Advanced Type

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!"

greeter1은 정상적으로 활동하지만 greeterMaker의 경우 특이하게 typeof라는 용어를 사용하게 되는 이는 Greeter 타입만 할당해줄 수 있다. 파생된 클래스의 인스턴스도 할당할 수 없다. 그리고 static의 특성 답게 greeterMaker에서 값을 바꾸면 greeter2에서도 값이 바뀐 것을 확인할 수 있다. 

 

 

 

'typescript > typescript-grammar' 카테고리의 다른 글

[typescript] Promise와 Async/Await 구문  (0) 2021.03.11
typescript - this and arrow functions  (0) 2020.03.06
5. typescript - interfaces  (0) 2020.03.03
4. typescript - 변수 선언  (0) 2020.02.29
3. typescript - 타입  (0) 2020.02.27