#4 Classes and interfaces

Haizel·2023년 4월 17일
0

🆃 TypeScript

목록 보기
3/4
post-thumbnail

💡Classes


TypeScript로 Class 만들기

class Player {
  //생성자 함수를 만들어준다.
  constructor(private firstName: string, protected lastName: string, public nickname: string
  ) {}
}

const nico = new Player("nico", "las", "니꼬");

nico.firstName
//--> firstName은 private로 수정 불가능하기 때문에 컴파일되지 않는다.

TypeScript에서만 사용할 수 있는 TypeScript만의 보호기능(JavaScript에서는 사용 불가)

  • private 키워드 : 선언한 클래스 내에서만 접근 가능
  • protected 키워드 : 선언한 클래스 내 + 상속받은 클래스 내에서 접근 가능
  • public 키워드 : 모두 접근 가능. public 은 생략 가능하다(즉 public이 기본값)
  • property 뿐만 아니라 method(클래스 안에 존재하는 함수)에서도 작동한다.

위 코드를 JavaScript로 작성한다면
class Player {
  //생성자 함수를 만들어준다.
  constructor(firstName, lastName, nickname) {
		this.firstName = firstName;
		this.lastName = lastName;
		this.nickname = nickname;
	}
}

nico.firstName
//--> firstName은 private로 수정 불가능해야 하는데, 수정 가능하다. 즉 JS에서는 private의 보호기능이 제공되지 않는다.

TypeScriptprivate, public 같은 보호기능은 제공되지 않는다.

private , protected , public 키워드는 TypeScript에서만 보여진다.


추상클래스(Abstract Class)

  • 추상클래스다른 클래스에 상속할 수 있는 클래스로, 직접 새로운 인스턴스를 만들 순 없다.

    ⇒ 즉 추상클래스는 오직 다른 곳에서 상속할수만 있는 클래스를 말한다.

//여기서 User는 추상클래스이다.
abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    public nickname: string
  ) {}
}

class Player extends User {
}

const nico = new Player("nico", "las", "니꼬"); // ⭕️
const nico = new User("nico", "las", "니꼬"); //❌ : 추상클래스는 새로운 인스턴스를 만들수 없다.

즉 User는 직접 인스턴스를 생성할 수 없고, 다른 클래스에 상속만 가능하다.

추상 클래스의 한계

  • JavaSCript에선 추상클래스라는 개념이 없기 때문에 → 추상클래스가 일반 클래스로 변환된다.

추상 클래스 속 메서드 만들기

//User : 추상클래스
abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    public nickname: string
  ) { }
		//추상 클래스 속 메서드 만들기
    getFullName() {
        return `${this.firstName} ${this.lastName}`
    }
}

class Player extends User {
}

const nico = new Player("nico", "las", "니꼬");

nico.getFullName();

추상 클래스 속 추상 메서드(Abstact method)만들기

  • 추상 메소드는 메서드(코드)를 구현하지 않고, Call signature만 작성한다.
  • 추상메소드는 상속받은 클래스에서 반드시 구현해야 하는 메서드이다.
//User : 추상클래스
abstract class User {
  constructor(
    private firstName: string,
    private lastName: string,
    private nickname: string
  ) {}
  //getNickname : 추상 메서드
  abstract getNickname(): void; // 추상메서드는 Call signature만 작성한다.

  //추상 클래스 속 메서드 만들기
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

class Player extends User {
  //getNickname 메서드를 구현하지 않으면 컴파일 되지 않는다.
    getNickname() {
			/*property를 private로 만들면, 상속받은 클래스에서도 해당 property에 접근할 수 없다.
				--> 따라서 nickname이 private로 만들어졌기 때문에 this로 접근할 수 없다
				- 위  private nickname -> protected nickname 로 수정하면 this로 접근할 수 있다.	 */
        console.log(this.nickname); //컴파일 되지 않음
    }
}

const nico = new Player("nico", "las", "니꼬");

nico.getFullName();

상속받은 클래스에 추상메소드가 있다면 → 자식 클래스에도 추상 메소드를 구현해주어야 한다.


📍 각 키워드의 접근 가능한 위치

키워드선언한 클래스 내상속받은 클래스 내인스턴스
private⭕️
protected⭕️⭕️
public⭕️⭕️⭕️

추상 클래스의 프로퍼티가 private이라면 → 추상 클래스를 상속받은 클래스에서도 해당 프로퍼티에 접근할 수 없다.
추상 클래스의 프로퍼티가 protected이라면 → 추상 클래스를 상속받은 클래스에서 해당 프로퍼티에 접근 가능하다.
단, private , protected 모두 클래스/인스턴스 밖에서는 접근 불가능하다.


생성자 함수의 프로퍼티 초기화


1️⃣ 생성자 함수가 프로퍼티를 자동 초기화

  • 타입스크립트
class Dict {
    constructor(
			private : words :Words
	) {}
}
  • 자바스크립트
    class Dict {
        constructor(words) {
            this.words = words;
        }
    }

2️⃣ 프로퍼티를 수동으로 초기화

: constructor에 인자로 넣어 constructor가 지정해주길 바라는게 아닐 경우

  • 타입스크립트
type Words = {
  // 프로퍼티의 이름을 모르지만 타입만 알때
  [key: string]: string;
};

class Dict {
    //1. words를 initializer(초기화) 없이 선언한다.
    private words: Words
		//2. constructor에서 수동으로 초기화 해준다.
    constructor() {
        this.words = {};
    }
}
  • 자바스크립트
    class Dict {
        constructor() {
            this.words = {};
        }
    }

클래스(Word)는 타입처럼 사용할 수 있다.

type Words = {
  // 프로퍼티의 이름을 모르지만 타입만 알때
  [key: string]: string;
};

//사전 클래스
class Dict {
  //1. words를 initializer(초기화) 없이 선언한다.
  private words: Words;
  //2. constructor에서 수동으로 초기화 해준다.
  constructor() {
    this.words = {};
  }
  //단어 추가하는 메서드
  add(word: Word) {
    //! 클래스(Word)를 타입처럼 사용할 수 있다. --> 즉 add의 파라미터가 Word클래스의 인스턴스이길 바라면 Word클래스를 타입으로 사용할 수 있다.
    //주어진 단어가 아직 사전에 존재하지 않다면
    if (this.words[word.term] === undefined) {
      this.words[word.term] = word.def;
    }
  }
  // term을 사용해 def를 찾는 메서드
  def(term: string) {
    return this.words[term];
  }
}

//단어 클래스
class Word {
  constructor(
      public term: string, //term = 용어
      public def: string 
  ) {}
}

const kimchi = new Word("kimchi", "한국음식");

const dict = new Dict();

dict.add(kimchi); // undefined
dict.def("kimchi"); // "한국음식"

메서드의 파라미터가 해당 클래스의 인스턴스이길 바라면 → 해당 클래스를 타입처럼 사용할 수 있다.


TypeScript의 클래스/메서드 코드 응용

type Words = {
  // 프로퍼티의 이름을 모르지만 타입만 알때
    [key: string]: string | string[]
};

//사전 클래스
class Dict {
  //1. words를 initializer(초기화) 없이 선언한다.
  private words: Words;
  //2. constructor에서 수동으로 초기화 해준다.
  constructor() {
    this.words = {};
  }
  //🔥 단어 추가하는 메서드
  add(word: Word) {
    //! 메서드에서 클래스(Word)를 타입처럼 사용할 수 있다.
    //주어진 단어가 아직 사전에 존재하지 않다면
    if (!this.words[word.term]) {
      this.words[word.term] = word.def;
    }
  }
  //🔥 term을 사용해 def를 찾는 메서드
  find(term: string) {
    return this.words[term];
  }
  //🔥 단어 삭제하기
  removeWord(term: string) {
    delete this.words[term];
  }
  //🔥 단어 이름 업데이트 하기
  updateWord(oldTerm: string, newTerm: string) {
    if (this.words.hasOwnProperty(oldTerm)) {
      this.words[newTerm] = this.words[oldTerm];
      delete this.words[oldTerm];
    }
  }
  //🔥 사전에 저장된 단어의 개수
    size() {
        return Object.keys(this.words).length;
    }
  //🔥 사전에 저장된 모든 term과 뜻 출력
    all() {
        for (let [key, value] of Object.entries(this.words)) {
            return `${key} : ${value}`
        }
    }
}

//각 단어에 대한  클래스
class Word {
  constructor(
    public term: string, //term = 용어
    public def: string | string[]
  ) {}
  //🔥 단어의 이름과 뜻을 출력하는 메서드
  toWord() {
    return `${this.term} : ${this.def}`;
  }
  //🔥 단어의 정의 추가
  addDef(newDef: string) {
    if (typeof this.def === "string") {
      this.def = [this.def, newDef];
    } else {
      this.def = [...this.def, newDef];
    }
  }
  //🔥단어의 정의 수정
    updateDef(oldDef : string, newDef: string) {
        if(typeof this.def === 'string') {
            if(oldDef === this.def) this.def = newDef
        } else {
            this.def.filter(el => el !== oldDef);
            this.def.push(newDef);
        }
    }
}

/*------- 출력 -------*/

// class Word
const kimchi = new Word("kimchi", "한국음식");
const tang = new Word("연근 갈비탕", "중국의 음식");
const sushi = new Word("스시", "일본의 음식");
kimchi.addDef("고춧가루로 배추를 버무려 숙성 및 발효시킨 음식");
kimchi.toWord(); // kimchi: 한국의 음식,고춧가루로 배추를 버무려 숙성 및 발효시킨 음식
tang.toWord(); // 연근 갈비탕: 중국의 음식
sushi.updateDef("일본의 음식", "밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식");
sushi.toWord(); // 스시: 밥을 뭉쳐놓고 그 위에 재료를 얹어낸 음식

//class Dict
const dict = new Dict();
dict.add(kimchi); // undefined
dict.add(tang); // undefined
dict.add(sushi); // undefined
dict.find("kimchi"); // "한국음식"
dict.all();
dict.find("kimchi"); // (2) ['한국의 음식', '고춧가루로 배추를 버무려 숙성 및 발효시킨 음식']
dict.size(); // 3
dict.updateWord("kimchi", "김치");
dict.removeWord("연근갈비탕")

💡Interfaces


  1. 인터페이스는 타입 체크를 위해 사용되며 변수, 함수, 클래스에 사용할 수 있다.
  2. ES6는 인터페이스를 지원하지 않지만 TypeScript는 인터페이스를 지원한다.
  3. 추상클래스의 한계(JS에서 추상클래스 → 일반클래스로 변환됨)를 개선할 수 있는데, 인터페이스는 컴파일하면 → JS로 바뀌지 않고 사라진다.
    • 즉 인터페이스는 타입스크립트에만 있는 용법으로, 인터페이스로 작성한 코드는 JS 컴파일 시 보이지 않는다.
    • 또한 인터페이스에 선언된 프로퍼티/메서드의 구현을 강제하여 → 일관성을 유지할 수 있도록 한다.

📍 클래스 vs 인터페이스

구분클래스인터페이스
프로퍼티와 메서드를 가질 수 있다.⭕️⭕️
직접 인스턴스 생성 가능⭕️
타입으로 사용 가능⭕️⭕️
메서드- 일반 메서드
- 추상메서드(abstract 키워드 사용)모두 추상메서드(abstract 키워드 사용❌)
JS로 컴파일⭕️

추상클래스 → 인터페이스로 바꾸기

  • 인터페이스는 오브젝트나 클래스의 모양을 특정지을 수 있다.
interface User {
    firstName: string;
    lastName: string;
    sayHi(name: string): string;
    fullName(): string;
}

//implements는 JS엔 없는 단어로, 코드가 좀 더 가벼워진다.
class Player implements User {
  constructor(
    //인터페이스를 상속할 때엔 → property는 private,protected로 만들 수 없다.
    public firstName: string,
    public lastName: string
  ) { }
    fullName() {
        return `${this.firstName} ${this.lastName}`
    }
    sayHi(name: string) {
        return `Hello ${name}. My name is ${this.fullName()}`;
    }
}

인터페이스를 상속할 때 → property는 private, protected 으로 만들 수 없다.


👍 인터페이스의 장점

  1. JS로 컴파일 시 추상클래스로 작성한 코드가 일반클래스로 변환되는 문제를 개선할 수 있다.

  2. JS로 컴파일 시 인터페이스로 작성한 코드는 사라지기 때문에 파일 크기가 줄어든다.

    • 인터페이스는 TS만 지원하기 때문이다.
  3. 하나 이상의 인터페이스를 동시에 상속할 수 있다.

    interface Human {
    	age :number
    }
    
    class Player implements User, Human {
      constructor(
        //인터페이스를 상속할 때엔 → property는 private,protected로 만들 수 없다.
        public firstName: string,
        public lastName: string
    		public age: number
      ) { }
    ...
    }

👎 인터페이스의 장점

  1. 인터페이스를 상속할 때 → 프로퍼티를 private, protected 으로 만들 수 없다.
  2. 인터페이스는 constructor함수를 생성할 수 없다.

인터페이스로 오브젝트 모양 지정하기

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

function makeUser(user: User) {
    return "hello"
}

makeUser({
    firstName: "nico",
    lastName: "las",
    fullName : () => "xx",
    sayHi: (name) => "string"
})

💡 타입과 인터페이스


1️⃣ 공통점   클래스나 오브젝트의 모양을 알려주고 싶을 때 사용 & 추상클래스를 대체할 수 있다.

  • 타입과 인터페이스 모두 → 타입스크립트에게 클래스나 오브젝트의 모양을 알려주고 싶을 때 사용한다.
  • 타입스크립트 커뮤니트에서는 클래스의 클래스나 오브젝트의 모양을 정의하고 싶을땐 인터페이스를, 다른 모든 경우엔 타입을 쓰라고 권장한다.

    인터페이스를 상속시키는 방법이 좀 더 직관적이기 때문에 → 규모가 큰 프로젝트에서는 인터페이스를 많이 사용한다.
    타입 alias나 특정값으로 타입을 제한 하는 경우에는 → 타입을 사용한다.

Type

type PlayerA = {
	name : string
}

class User implements PlayerA {
	constructor(
		public name :string
	)
}

Interface

interface PlayerB {
	name : string
}

class User implements PlayerB {
	constructor(
		public name :string
	)
}

2️⃣ 차이점 🔥 상속방법

Type

  • 타입에 새로운 프로퍼리를 추가하고 싶다면 타입을 만들고 && 연산자로 상속해야 한다.
type PlayerA = {
    name :string
}

type PlayerAA = PlayerA & {
    lastName: string;
};

const playerA: PlayerAA = {
    name: "nico",
    lastName : "las"
}

Interface

  • extends 사용하는 방법
    interface PlayerB {
      name: string;
    };
    
    interface PlayerBB extends PlayerB{
      lastName: string;
    };
    
    const playerB: PlayerBB = {
      name: "nico",
      lastName: "las",
    };
  • 정의된 인터페이스를 재활용(But kinda Dumb)
interface PlayerB {
  name: string;
};

interface PlayerB {
  lastName: string;
};

const playerB: PlayerB = {
  name: "nico",
  lastName: "las",
};

❗ 새 프로퍼티 추가를 위해 타입은 다시 선언 될 수 ❌ ↔️ 인터페이스는 다시 선언 가능(항상 상속 가능)


💡 다형성 + 제네릭 + 클래스 + 인터페이스


  1. 다형성 : 다른 모양의 코드를 가질 수 있게 해주는 것, 다형성을 이루는 방법은 → 제네릭을 사용하는 것이다.
  2. 제네릭 : concrete타입이 아닌 placeholder 타입을 사용할 수 있게 한다.

로컬 스토리지 API 따라해보기

interface SStorage<T> {
    //3. 인터페이스에서 제네릭을 받아 사용한다.
    [key:string] : T
}

//1. 제네릭을 클래스로 보내고
class LocalStorage<T> {
  //2. 클래스는 해당 제네릭을 인터페이스로 보내준다.
    private storage: SStorage<T> = {}

		set(key:string, value:T){
			this.storage[key] = value;
		}
		get(key:string) :T {
			return this.storage[key]
		}
		remove(key:string){
			delete this.storage[key]
		}
		clear(){
			this.storage = {}
		}
}

const stringsStorage = new LocalStorage<string>()
stringsStorage.get("key") //get(ket:string) :string --> 즉 제네릭 <T>의 타입(placeholder)이 string 타입(concrete)이 된다.
stringsStorage.set("hi", "how are you")

const booleansStorage = new LocalStorage<boolean>()
booleansStorage.get("xxx")
booleansStorage.set("hi", false)
profile
한입 크기로 베어먹는 개발지식 🍰

0개의 댓글