이 글은 제가 타입스크립트를 공부하면서 정리한걸 옮겨온 것입니다.

타입스크립트(typescript)의 class에 관한 주요 개념을 알아보자.

자바스크립트(javascript)는 프로토타입 기반의 객체지향 프로그래밍이 가능하다.

ES6에는 JavaC++ 처럼 class가 생겼다.

하지만 다른 객체 지향 언어에 비해 자바스크립트 class는 기능이 적다.

타입스크립트를 사용하면 더욱 확장된 기능을 사용할 수 있다.

기본 사용법

class Person {
    name: string
    constructor(name: string) { // (public name: string)으로 속성 정의를 단축할 수 있다.
        this.name = name
    }
    greet(): string {
        return 'Hello, ' + this.name
    }
}

let ash = new Person('ash')

객체 지향 프로그래밍에 익숙하다면 위 문법 또한 익숙하다.

Person class를 만들고 new 키워드로 인스턴스를 생성했다.

name이라는 프로퍼티와 greet이라는 메소드를 가진다.

class Person {
    name: string
    constructor(name: string) {
        this.name = name
    }
    greet(): string {
        return 'Hello, ' + this.name
    }
}

class SalesMan extends Person {
    constructor(name: string) {
        super(name)
    }
    greet(): string {
        return 'Hello, i am salesMan, ' + this.name
    }
    work() {
        console.log('I am working')
    }
}

let ash = new SalesMan('ash')

부모 클래스를 상속받는 것 또한 가능하다.

예제 코드에서는 Person이라는 클래스를 상속해서 SalesMan이라는 클래스를 작성했다.

greet 메소드를 재작성(오버라이딩)하고 work 메소드를 새로 추가했다.

자식 메소드의 생성자 함수(constructor)에서 super함수를 실행했는데,

이는 부모의 생성자 함수를 가리킨다.

반드시 따라야한다.

접근 제어자

ES6의 기본 classpublic 접근제어자밖에 사용할 수 없다.

이 말은 인스턴스의 멤버를 외부에서 마음껏 접근 가능하다는 의미이다.

타입스크립트에선 기본 자바스크립트이 느슨함을 조이기 위해

접근 제어자를 public, private, protected로 구분한다.

class Person {
    private name: string
    protected pName: string
    constructor(name: string) {
        this.name = name
        this.pName = name
    }
    greet(): string {
        return 'Hello, ' + this.name
    }
}

class SalesMan extends Person {
    greet(): string {
        return 'Hello, i am salesMan, ' + this.name // 오류 발생
    }
    greetP(): string {
        return 'Hello, i am salesMan, ' + this.pName // 참조 가능
    }
    constructor(name: string) {
        super(name)
    }
    work() {
        console.log('I am working')
    }
}

let ash = new SalesMan('ash')
ash.pName // 오류 발생

public은 객체 외부, 내부에서 모두 접근 가능하다.

접근 제어자를 별도로 선언하지 않으면 기본값이기도 하다.

위 코드에서 Person 클래스에서 private로 name 멤버를 선언했다.

pNameprotected로 선언했다.

private 멤버는 상속받는 자식 클래스나 외부 어디서도 접근이 불가능하지만,

protected 멤버는 상속받는 자식 클래스에선 접근이 가능하다.

readonly

class Person {
    name: string;
    readonly createdAt: Date;
    constructor(name: string) {
        this.name = name
        this.createdAt = new Date()
    }
}

let ash = new Person('ash')
ash.createdAt = new Date() // 오류 발생!

readonly 키워드를 사용하면 읽기만 가능한 속성을 지정할 수 있다.

createdAt 처럼 인스턴스가 최초 생성된 날짜를 기록하는 속성의 경우

추후에 수정되는 안되니까 읽기 전용으로 만들 수 있다.

class Person {
    constructor(readonly name: string) {
    }
}

let ash = new Person('ash')
ash.name = 'new ash' // 오류 발생!

생성자 함수를 생성할 때 인자로 받아서 readonly를 지정하고 싶은 경우

예시처럼 단축문법으로 할 수도 있다.

getter, setter

class Person {
    private _address: string;
    constructor(readonly name: string, address: string) {
        this._address = address
    }
    get address() {
        return this._address
    }
    set address(newAddress: string) {
        this._address = newAddress
    }
}

let ash = new Person('ash', '노원구')
console.log(ash.address) // 노원구
ash.address = '중랑구'
console.log(ash.address) // 중랑구

보통 정보 은닉을 위해 프로퍼티를 외부에서 접근할 수 없게 private로 지정하고,

이를 접근할 수 있는 getter, 변경할 수 있는 setter 메소드를 선언해서 사용한다.

private 프로퍼티의 경우 앞에 _를 붙이는 컨벤션이 있다.

static 프로퍼티

ES6 기본 클래스에도 스태틱 프로퍼티를 지정할 수 있지만,

메소드에만 지정이 가능하다.

타입스크립트에는 스태틱 속성, 메소드 둘 다 가능하다.

class Robot {
    static createdCount = 0;
    constructor(readonly name: string) {
        Robot.createdCount++
    }
    static showCreatedCount() {
        console.log(Robot.createdCount)
    }
}

let i1 = new Robot('i1')
let i2 = new Robot('i2')
let i3 = new Robot('i3')
Robot.showCreatedCount() // 3
console.log(Robot.createdCount) // 3

Robot 클래스를 만들고 인스턴스가 생성될 떄 마다 증가되는 스태틱 카운트와

이를 출력해주는 스태틱 메소드를 만들어 보았다.

추상 클래스(Abstract Classe)

추상 클래스는 반드시 상속(또는 구현)해서 사용해야 한다.

추상 클래스 자체로는 new ... 으로 객체를 생성할 수 없다.

또한 추상 메소드를 가질 수 있는데,

이는 구체적인 구현이 없어야 하고 반드시 상속받는 클래스에서 구현해야한다.

abstract class Person {
    constructor(public name: string) {
    }
    abstract eat() // 반드시 오버라이딩 해야한다.
}

class Developer extends Person {
    constructor(name: string) {
        super(name)
    }
    eat() {
        console.log('냠냠냠')
    }
}

class SalesMan extends Person { // 오류 발생!
    constructor(name: string) {
        super(name)
    }
}

let ash = new Person('ash') // 오류 발생!
let devAsh = new Developer('ash')
devAsh.eat() // 냠냠

위 예제에선 new Person() 에서 오류가 발생하고

eat을 구현하지 않은 SalesMan 클래스에서도 오류가 발생한다.

타입(type) 또는 인터페이스(interface) 처럼 사용하기

class Person {
    constructor(public name: string) {
    }
    hello(): void {
        console.log('Hello, i am ' + this.name)
    }
}

let person: Person
person = 'ash' // 오류 발생
person = new Person('ash')

interface IPerson {
    name: string
    hello(): void
}

let person2: IPerson
person2 = 'sss' // 오류 발생

클래스도 인터페이스와 유사하게 타입처럼 사용할 수 있다.

class Point {
    x: number
    y: number
}

interface Point3d extends Point {
    z: number
}

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

또 인터페이스가 클래스를 확장(또는 상속) 할 수 있다.

생각해볼 것

  • 클래스를 타입처럼 사용하는 상황

  • 추상 클래스를 사용하는 이유

  • 인터페이스가 클래스를 확장하는 상황