본문 바로가기

프로그래밍/TypeScript

Class in TypeScript

반응형

TypeScript는 Javascript ES6부터 도입된 class keyword를 서포트한다.
자바스크립트의 클래스는 다른 언어들에 통상적으로 존재하는 제한자를 다양하게 지원하지는 않는데, 타입스크립트에서 이 부분을 보완해준다는 점에서 유용할 수 있다.

public

어디서든 접근할 수 있는 멤버이다. keyword 없이 정의하면 자동으로 public 멤버가 되며 public 키워드로 명시적으로 나타낼 수도 있다.

protected

서브클래스와 클래스 정의 내부에서만 접근 가능한 멤버이다.

  • 서브클래스 정의시 상속받은 멤버의 visibility를 override하여 변경할 수는 있다.
  • 같은 부모 클래스로부터 파생된 다른 서브클래스의 protected member에 접근할수는 없다.

private

클래스 정의 내부에서만 접근 가능한 멤버이다. 서브클래스에서도 접근하지 못한다.

  • 파생 클래스에서 상속받은 멤버의 visibility도 변경할 수 없다.
  • cross-instance private 접근이 가능하다. 즉, 같은 클래스내의 다른 인스턴스 멤버에 서로 접근이 가능하다.
class A {
    private x = 10;
    public sameAs(other: A) {
        // No error
        return other.x === this.x;
    }
}

TS의 private vs JS의 private accessibility modifier(#)

TypeScript의 visibiliity 제한자는 여타 다른 타입스크립트 문법과 마찬가지로 컴파일시점의 타입체킹 동안에만 유효하다.
즉, JavaScript 런타임에서는 in, bracket notation 등으로 private, protected 요소에 액세스할 수 있다. (soft private)
JavaScript로 트랜스파일링된 결과물을 봐도 public, private, protected 속성별 차이가 없다.

class MySafe {
    private secretKey = 12345;
}

const s = new MySafe();

// Not allowed during type checking
console.log(s.secretKey);

// OK
console.log(s["secretKey"]);

ES2022이후부터 JavaScript에서도 class 내부 속성 선언시 앞에 #을 붙여 private 멤버로 생성할 수 있다.
이렇게 명시된 필드는 런타임에서도 private하게 남아있다. (hard private)
아래 예시코드의 #barkAmount는 외부에서 접근이 불가능하다.

class Dog {
  sound = "bow wow";
  #barkAmount = 0;

  constructor() {}

  bark(){
    console.log(this.sound);
    this.#barkAmount += 1;
  }
}

const dog = new Dog();

dog.bark();

// error. Property '#barkAmount' is not accessible outside class 'Dog' because it has a private identifier.
console.log(dog.#barkAmount)

위의 코드는 ES2022이전 버전에서는 다음과 같이 WeakMap을 이용하는 방식으로 트랜스파일링된다.

"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _Dog_barkAmount;
class Dog {
    constructor() {
        this.sound = "bow wow"; // public 멤버는 class 내부에 정의되고
        _Dog_barkAmount.set(this, 0); // private 멤버는 외부의 WeakMap에 저장한다.
    }
    bark() {
        console.log(this.sound);
        __classPrivateFieldSet(this, _Dog_barkAmount, __classPrivateFieldGet(this, _Dog_barkAmount, "f") + 1, "f");
    }
}
_Dog_barkAmount = new WeakMap();
const dog = new Dog();
dog.bark();

static

클래스의 특정 인스턴스와 연관되지 않고 클래스 constructor object 자체로 접근가능한 멤버이다.
visibility modifiers(public, protected, public)와 같이 쓰일 수 있다.

  • 이때 static method의 이름은 Function의 built-in static name들과 겹치지 않아야한다. (JavaScript에서 class는 Function이기 때문이다)

readonly

수정 불가능한 읽기전용 멤버임을 의미한다. visibility modifiers(public, protected, public)와 같이 쓰일 수 있다.

Parameter Properties

contructor의 parameter에 제한자를 명시하는 방법으로도 클래스내 멤버를 선언할 수 있다.

class Params {
      public readonly x:number;
    protected y: number;
    private z: number;
    constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
}

위의 코드를

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}

parameter properties를 이용하여 축약해서 작성하는 것이 가능하다.

--strictPropertyInitialization

--strictPropertyInitialization 옵션을 사용하면 내부 속성이 꼭 초기화단계에 정의되어야한다.
꼭 constructor에서 직접 초기화되어야하며 다른 method를 호출하여 method내에서 초기화 되면 오류가 발생한다. (파생 클래스가 메서드 오버라이드하면서 초기화 누락될 수 있기때문.)
만약 contructor에서 초기화되지 않는 것을 의도한다면 변수명 옆에 definite assignment assertion operator(!)를 사용하면 된다.

class OKGreeter {
  // Not initialized, but no error
  name!: string;
}
반응형