Languages/TypeScript

[노마드코더] #4 Classes and Interfaces

성중 2022. 5. 21. 21:50

Classes

TypeScript는 JavaScript 객체 지향 프로그래밍(OOP)을 위한 다양한 기능을 지원한다

class Player {
    constructor(
        private firstName: string,
        private lastName: string,
        public nickName: string
    ) {}
}

이렇게 TS에서 클래스의 private/public 속성(property), 타입을 정의하는 것 만으로..

 

class Player {
    constructor(firstName, lastName, nickName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.nickName = nickName;
    }
}

JS 상에서 클래스의 property가 정의되고 동작한다!

 

const me = new Player("sungjoong", "kim", "joseph")

//error
me.firstName;
//pass
me.nickName;

인스턴스를 생성, public/private 여부에 따라 클래스 외부에서의 property 접근이 제한된다

 

abstract class User {
    constructor(
        private firstName: string,
        private lastName: string,
        public nickName: string
    ) {}
    private getFullName(){
        return `${this.firstName} ${this.lastName}`
    }
}

class Player extends User {
}

// error
const me = new User("sungjoong", "kim", "joseph");
// error
me.getFullName();

인스턴스를 직접 생성할 수 없고, 다른 클래스가 상속만 받을 수 있는 추상 클래스도 정의할 수 있다

* 메서드(method)에도 private/public 지정이 가능

 

abstract class User {
    constructor(
        private firstName: string,
        private lastName: string,
        protected nickName: string
    ) {}
    abstract getNickName(): string
    getFullName(){
        return `${this.firstName} ${this.lastName}`
    }
}

class Player extends User {
    getNickName() {
        return this.nickName;
    }
}

const me = new Player("sungjoong", "kim", "joseph");
me.getNickName;

추상 클래스에 메서드의 Call Signature를 넣는 방식으로 추상 메서드를 정의할 수 있다

 

public/private/protected의 property/method 접근 차이

  • public: 정의된 클래스 외부에서 접근할 수 있다 (기본값)
  • private: 정의된 클래스 내부에서만 접근할 수 있다
  • protected: 정의된 클래스 내부 + 추상 클래스를 상속받은 클래스 내부에서 접근할 수 있다

 


type Words = {
   [key: string]: string 
}

class Dict {
    private words: Words;
    constructor() {
        this.words = {};
    }
    add(word: Word) {
        if(this.words[word.term] === undefined){
            this.words[word.term] = word.def;
        }
    }
    def(term: string) {
        return this.words[term];
    }
    static hello() {
        return "hello";
    }
}

class Word {
    constructor(
        public readonly term: string,
        public readonly def: string
    ) {}
}

const kimchi = new Word("김치", "한국의 음식");
const dict = new Dict();
dict.add(kimchi);
dict.def("김치");
Dict.hello();
  1. 객체의 전체 타입 지정 (index signature) 타입 선언
  2. constructor에 포함되지 않은 형태로 property를 정의하고, 이후 수동으로 초기화
  3. 클래스를 타입처럼 사용 (클래스의 인스턴스가 매개변수인 경우)
  4. 클래스의 static 메서드/변수는 인스턴스와 관계없이 호출 가능
  5. 변경을 원하지 않는다면 property에서도 readonly 속성을 활용

Interfaces

type Team = "red" | "blue" | "yellow"
type Health = 1 | 5 | 10

type Player = {
    nickname: string,
    team: Team,
    health: Health
}

const me: Player = {
    nickname: "joseph",
    team: "blue",
    health: 5
}

concrete type이 아닌, 특정 값들 중에서만 선택할 수 있도록 타입을 지정할 수도 있다

 

interface Player {
    nickname: string,
    team: Team,
    health: Health
}

오로지 객체의 모양을 설명하기 위한 목적으로 type을 interface로 대체할 수 있다

 

interface User {
    name: string
}
interface Player extends User {
}

type User2 = {
    name: string
}
type Player2 = User2 & {
}

type과 달리 클래스처럼 상속이 가능하다

 

interface User {
    name: string
}
interface User {
    age: number
}

const me: User = {
    name: "joseph",
    age: 23
}

type과 달리 중복 선언이 가능하며, 자동으로 합쳐진다 (= property가 쌓인다)

 


추상 클래스는 JS로 컴파일시 일반 클래스로 남는 반면, interface는 컴파일시 사라진다

즉, 추상 클래스를 interface로 대체할 수 있다면 JS 코드가 더 가벼워진다!

 

abstract class User {
    constructor(
        protected firstName: string,
        protected lastName: string
    ) {}
    abstract sayHi(name: string): string
    abstract fullName(): string
}

class Player extends User {
    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    sayHi(name: string) {
        return `Hello ${name}. My name is ${this.fullName()}`;
    }
}

위와 같은 추상 클래스를 interface로 대체해보자

 

interface User {
    firstName: string,
    lastName: string,
    sayHi(name: string): string,
    fullName(): string
}

class Player implements User {
    constructor(
        public firstName: string,
        public lastName: string,
    ) {}
    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    sayHi(name: string) {
        return `Hello ${name}. My name is ${this.fullName()}`;
    }
}

이처럼 클래스가 interface를 implements로 상속받고, property를 constructor에 public으로 정의해주면 된다 (type도 동일하게 가능)

 

interface User {
    firstName: string,
    lastName: string,
    sayHi(name: string): string,
    fullName(): string
}

interface Human {
    health: number
}

class Player implements User, Human {
    constructor(
        public firstName: string,
        public lastName: string,
        public health: number
    ) {}
    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
    sayHi(name: string) {
        return `Hello ${name}. My name is ${this.fullName()}`;
    }
}

function makeUser (user: User){
    console.log(user);
}

makeUser({
    firstName: "kim",
    lastName: "sungjoong",
    fullName: () => "full name",
    sayHi: (name) => `hi ${name}`
});

클래스는 여러 개의 interface들을 동시에 상속받을 수 있다!

* interface 역시 매개변수의 타입처럼 사용이 가능하며, 클래스처럼 매개변수에 인스턴스를 생성해 넣어줄 필요 없이 interface의 형태만 맞추면 동작

 

🔽공식문서 Type Aliases vs Interfaces 비교

 

Documentation - Everyday Types

The language primitives.

www.typescriptlang.org

interface는 type과 거의 유사하고 대체될 수 있지만 클래스나 객체의 형태를 정의하는 경우, 재선언이 가능하고 확장이 직관적인 interface 사용을 권장한다

 


Local Storage API 클래스를 가정해 타입을 만들어보며 다형성(Polymorphism)을 구현해보자

interface IStorage<T> {
    [key: string]: T
}

class LocalStorage<T> {
    private storage: IStorage<T> = {}
    set(key: string, value: T) {
        this.storage[key] = value;
    }
    remove(key: string) {
        delete this.storage[key];
    }
    get(key: string): T {
        return this.storage[key];
    }
    clear() {
        this.storage = {};
    }
}

const stringStorage = new LocalStorage<string>();
stringStorage.set("hello", "hi");
stringStorage.get("hello");

const booleanStorage = new LocalStorage<boolean>();
booleanStorage.set("hello", true);
booleanStorage.get("hello");

클래스에 할당한 Generic을 클래스에 속한 interface가 받아서 공유할 수 있다!

 

본 내용은 노마드코더의 'Typescript로 블록체인 만들기'를 바탕으로 작성되었습니다.