/ #타입스크립트

클래스와 인터페이스

클래스(class)

클래스(class)는 자바스크립트 es6에 추가된 타입입니다. es6 이전에는 클래스가 없고 프로토타입을 기반으로 클래스를 만들었으나, 클래스가 추가되면서 조금 더 명료하게 클래스를 작성할 수 있게 되었습니다.

클래스 선언

클래스 선언은 다음과 같이 합니다.

class User { }

함수의 선언과 달리 클래스는 호이스팅 되지 않습니다. 선언된 클래스는 new 키워드를 통해 인스턴스화(Instantiation) 될 수 있습니다.

let user: User = new User(); 

클래스 생성자

생성자는 constructor 키워드를 통해 정의할 수 있습니다.

class User {
    constructor(name: String) {
        console.log(name);
    }
}

let user: User = new User("rubisco"); 

인스턴스 속성

객체의 속성과 마찬가지로 인스턴스도 속성을 가질 수 있으며, 클래스 내부에서는 this 키워드를 통해 접근 가능합니다. 또한 다른 언어들과 비슷하게 접근자 키워드를 통해 속성 접근을 제한할 수 있습니다.

접근자에는 public, protected, private가 있으며, 기본값은 public입니다.

타입스크립트 컴파일시 환경설정에 --strictPropertyInitialization 옵션이 있다면 반드시 속성을 초기화 해주어야 합니다.

class User {

    private name: string;

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

let user: User = new User("rubisco"); 

만약 읽기전용 속성을 생성하려면 자바에서 final 키워드를 붙이는 것과 같이 readonly 키워드를 속성 앞에 붙입니다.

class User {

    private readonly name: string;

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

let user: User = new User("rubisco"); 

Getter / Setter

자바스크립트의 클래스 또한 Getter와 Setter를 선언할 수 있습니다. 메소드 앞에 get 키워드를 붙이면 getter, set 키워드를 붙이면 setter가 됩니다.

class User {

    private _name: string;

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

    get name() {
        return this._name;
    }

    set name(name: string) {
        this._name = name;
    }
}

let user: User = new User("rubisco");

console.log(user.name); // "rubisco" 출력

user.name = "spot";
console.log(user.name); // "spot" 출력

클래스의 확장

자바와 마찬가지로 타입스크립트의 클래스도 extends 키워드를 통해 상속이 가능합니다. 또한 super 키워드를 통해 부모 클래스에 접근할 수 있습니다.

class Animal {
    
    private _name: string;

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

    get name() {
        return this._name;
    }

    set name(name: string) {
        this._name = name;
    }
}

class Dog extends Animal {

    constructor(name: string) {
        super(name);
    }

    sniff(): void {
        console.log(`${ this.name }이(가) 냄새를 맡습니다.`);
    }
}

class Bird extends Animal {

    constructor(name: string) {
        super(name);
    }

    fly(): void {
        console.log(`${ this.name }이(가) 날아다닙니다.`);
    }
}

let dog: Dog = new Dog("코커스파니엘");
let bird: Bird = new Bird("까마귀");

dog.sniff();
bird.fly();

추상 클래스

추상 클래스는 클래스 앞에 abstract 키워드를 통해 선언할 수 있습니다. 일반 클래스와 달리 추상 클래스는 인스턴스화가 불가능하며, 추상 클래스를 상속받은 자식 클래스에게 추상 메소드의 구현을 강제합니다.

abstract class Animal {
    
    private _name: string;

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

    get name() {
        return this._name;
    }

    set name(name: string) {
        this._name = name;
    }

    abstract move(): void;
}

class Dog extends Animal {

    constructor(name: string) {
        super(name);
    }

    move(): void {
        console.log("네발로 걷습니다.");
    }

}

class Bird extends Animal {

    constructor(name: string) {
        super(name);
    }

    move(): void {
        console.log("날개로 날아다닙니다.");
    }
}

let dog: Dog = new Dog("코커스파니엘");
let bird: Bird = new Bird("까마귀");

dog.move();
bird.move();

인터페이스(Interface)

타입스크립트의 핵심 원리 중 하나는 값이 가지는 형태에 초점을 맞추어 타입체크를 한다는 것입니다. 이러한 동적 타이핑을 덕 타이핑(duck typing)이라고 합니다.

“어떤 새가 오리처럼 보이고 오리처럼 헤엄치는 오리처럼 운다면 그것은 오리일 것이다.”

덕 타이핑은 덕 테스트에서 유래한 단어로, 타입스크립트에서 데이터의 타입은 인터페이스 형태에 의하여 결정됩니다.

자바스크립트만 다뤄 본 개발자들은 인터페이스라는 개념이 생소하게 느껴질 것입니다. 인터페이스는 자바나 C# 등 정적 타입 언어에서 이미 많이 쓰이고 있는 개념으로, 여러가지 타입을 가지는 프로퍼티로 이루어진 새로운 타입을 정의하는 규칙입니다. 인터페이스에 선언된 프로퍼티나 메소드의 구현을 강제하여 일관성을 유지하도록 하는 역할을 합니다.

인터페이스는 타입의 이름을 짓는 코드 내부의 계약을 정의할 뿐만 아니라 프로젝트 외부에서 사용하는 코드의 계약을 정의하는 강력한 방법입니다. 프로퍼티와 메소드를 가진다는 점에서 클래스와 유사하지만 직접적으로 인스턴스를 생성할 수 없으며, 모든 메소드는 추상 메소드로 구성됩니다.

인터페이스 정의

인터페이스를 정의하는 문법은 객체(object) 타입과 유사합니다.

let user: {
    index: number;
    name: string;
    level: number;
};

위에 객체를 인터페이스로 정의하면 다음과 같습니다.

interface User {
    index: number;
    name: string;
    level: number;
};

인터페이스나 클래스는 파스칼 표기법(Pascal case)를 사용하여 정의하는 것이 관례이며, 정의된 인터페이스는 다음과 같이 사용가능합니다.

let user: User = { index: 1, name: 'rubisco', level: 4 };

객체 타입과 마찬가지로 읽기 전용 속성 또는 선택 속성을 정의할 수도 있습니다.

interface User {
    readonly index: number;
    name: string;
    level: number;
    item?: string;
};

let player: User = { index: 2, name: 'spot', level: 4 };

함수형 인터페이스(Functional Interface)

인터페이스를 통해 함수 타입을 정의하는 것도 가능합니다. 함수 타입을 정의하기 위해서는 호출 시그니쳐(call signature) 즉, 매개변수의 타입 정의가 필요합니다.

interface User {
    readonly name: string;
    hp: number;
    mp: number;
    item?: string;
};

interface GetUserName {
    (player: User): string;
}

const getUserName: GetUserName = function(user) {
    return user.name;
}

위의 코드를 보면 GetUserName이라는 인터페이스를 통해 해당 타입의 함수는 User 타입의 매개변수를 받고 문자열을 반환한다는 것을 알 수 있습니다.

함수형 인터페이스를 통해 람다식을 작성함으로써 코드를 간결하게 표현할 수도 있습니다.

const getUserNameLambda: GetUserName = (user) => user.name;

하이브리드 타입(Hybrid Type)

호출 시그니처와 속성 타입을 동시에 가지는 인터페이스를 하이브리드 타입(Hybrid Type)이라고 합니다.

interface Player {
    readonly name: string;
    hp: number;
    mp: number;
    power: number;
    items?: string[];
};

interface Battle {
    (player: Player, opponent: Player): Battle;
    attack(): void;
    turn: number;
}

let battle = <Battle> function (player: Player, opponent: Player): Battle {
    if (battle.turn == null) battle.turn = 0;
    battle.turn += 1;
    battle.attack = () => player.hp -= opponent.power;
    return battle;
};

let player1: Player = { name: 'rubisco', hp: 50, mp: 10, power: 10 };
let player2: Player = { name: 'enemy', hp: 50, mp: 10, power: 10 };

battle(player1, player2).attack();

console.log(battle.turn, player1.hp);

위에 코드에서 Battle 인터페이스는 호출 시그니처와 속성을 모두 가진 하이브리드 타입 입니다.

인덱서블 타입 (Indexable Types)

인덱서블 타입은 타입을 인덱스로 사용할 수 있도록 합니다. 자바에서 list와 map 타입이 인덱서블 타입에 해당합니다. 인덱서의 타입은 numberstring, symbol만 지정할 수 있습니다.

interface User {
    readonly name: string;
};

interface UserArray {
    [index: number]: User;
}

let user1: User = { name: "Kim" };
let user2: User = { name: "Lee" };

let users:UserArray = [user1, user2];

console.log(users[1].name);

인터페이스 확장

인터페이스는 자바와 마찬가지로 extends 키워드를 통해 상속이 가능합니다.

interface GameObject {
    name: string;
    hp: number;
    mp?: number;
}

interface Player extends GameObject {
    attack(): void;
    defense(): void;
}

let player: Player = {
    name: "rubisco",
    hp: 50,
    mp: 10,
    attack: function (): void {
        console.log("공격!");
    },
    defense: function (): void {
        console.log("방어!");
    }
}

player.attack();

인터페이스의 경우 클래스와 달리 extends 키워드를 통해 다중 상속이 가능합니다.

interface GameObject {
    name: string;
    hp: number;
    mp?: number;
}

interface Attack {
    attack(): void;
}

interface Defense {
    defense(): void;
}

interface Player extends GameObject, Attack, Defense {}

let player: Player = {
    name: "rubisco",
    hp: 50,
    mp: 10,
    attack: function() {
        console.log("공격!");
    },
    defense: function() {
        console.log("방어!");
    },
}

player.attack();

클래스의 인터페이스 구현

클래스에서 인터페이스를 구현하기 위해서는 implements 키워드를 사용합니다.

interface Animal {
    move(): void;
}

class Dog implements Animal {
    
    move(): void {
        console.log("네발로 걷습니다.");
    }
}

let dog: Dog = new Dog();

dog.move();

구현을 강제한다는 점에서 추상 클래스와 비슷하지만 추상 클래스와 달리 인터페이스는 다중 상속이 가능합니다. 상황에 따라 중복된 기능을 가지며 같은 조상으로 부터 물려받을 것이 보장되길 원한다면 추상클래스를, 기능이 중복되지만 조상이 다르다면 인터페이스를 사용하면 됩니다.