클래스

홍범선·2023년 10월 24일
0

타입스크립트

목록 보기
23/34

타입스크립트 클래스

class SampleClass {

}
// 클래스 안에서 프로퍼티를 사용할 떄 타입을 입력해주어야함, 초기화도 해주어야함
class Game {
  name:string;
  country: string;
  download: number;

  constructor(name: string, country: string, download:number){
    this.name = name;
    this.country = country;
    this.download = download
  }

  introduc(){
    return `${this.name}${this.country}에서 제작된 ${this.download}개의 다운로드를 달성한 게임입니다.`;
  }
}

const starcraft = new Game(
  'star craft',
  'USA',
  10000000,
  // true //에러
);

const retVal = starcraft.introduc();
starcraft.changeGame(); //JS는 실행해야지 런타임 에러가 발생한다. TS는 런타임전에 알려준다.

클래스 안에서 프로퍼티를 사용할 떄 타입을 입력해주어야 한다.
초기화는 생성자 함수해서 진행한다.

만약 생성자 함수에 지정된 타입과 파라미터 개수등이 일치하지 않으면 에러가 발생한다.
또한 클래스에 존재하지 않는 메서드를 호출할 때도 TS는 런타임전에 에러를 알려준다.

readonly

class Idol {
  readonly name: string;
  age: number;

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

const yuJin = new Idol('안유진', 23);

yuJin.age = 32;
yuJin.name= '코드팩토리'; //리드온리기 때문에 에러가 발생한다. JS에서는 readonly는 작동하지 않는다.

프로퍼티 초기화

class Person{
  //일반적인 필수값 선언법
  name: string;
  //초기값 제공 선언법
  age : number = 23;
  //optional 값 선언법
  pet? : string; // 값을 넣어도 되고 안넣어도 되기 때문
  //type of undefined 선언법
  petAge: number | undefined;


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

class RouteStack{
  stack! : string[];  
//느낌표는 초기화가 보장이 됬다고 알 수 있다. 
//initalize안에서 this.stack을 초기화 하고 있는지 안하고 있는지 TS는 알지 못한다. 
//그 때 느낌표를 사용해서 무조건 초기화 된다고 보장한다.

  constructor(){
    this.initalize();
  }

  initalize(){
    this.stack = [];
  }
}
const routeStack = new RouteStack();

프로퍼티 초기화 하는 방법은 5가지 정도가 있다.
1. 일반적인 필수값 선언법 => constructor에서 초기화
2. 초기값 제공 선언법 => 선언하자마자 초기화
3. optional 값 선언법 => optional이란 넣어도 되고 안넣어도 되는 거이기 떄문에 초기화 하지 않아도 에러발생 하지 않는다.
4. type of undefined => 타입 | undefined로 유니온하면 된다.
5. ! => !는 초기화가 보장됬다고 강제로 지정할 수 있다. constructor에서 초기화 함수를 넣어주면 된다.

클래스 타입

class Dog{
  name: string;

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

  bark(){
    return `${this.name}이 짖습니다.`
  }
}

let ori = new Dog('오리');
console.log(ori.bark()); //Dog타입, 클래스 자체가 타입이 될 수 있다.

// ori = "오리"; //에러 발생 Dog타입이 아니기 떄문에
ori = {
  name:'별이',
  bark(){
    return `${this.name}가 짖습니다.`
  }
}

클래스 자체가 타입이 될 수 있다.
ori변수에는 Dog타입이다.
그래서 ori = "오리"를 하게 되면 string타입은 Dog타입이 아니기 때문에 에러가 발생한다.

반면에 ori를 객체로 클래스에 시그니처들을 가져다가 사용하면 에러가 발생하지 않는다.
왜냐하면 자바스크립트는 객체로 클래스를 만들기 때문에 객체와 클래스가 같은 타입이다.
이 시그니처만 같으면 객체로도 같은 클래스 타입이라고 생각한다.

인터페이스

/**
 * Interface
 * 인터페이스란 어떤 클래스가 어떤 시그니처를 강제할 수 있게 해주는 것
 */

interface Animal{
  name:string;
  age:number;
  jump() : string;
}

class Dog implements Animal{ 
  // 인터페이스를 implements를 하게 되면 해당 요소 전부 클래스 안에 똑같이 정의되어야 함
  name: string;
  age: number;

  constructor(name:string, age:number){
    this.name = name;
    this.age = age;
  }
  jump(): string {
    return `${this.name}이 점프를 한다.`
  }
  dance(){

  }
}// Animal의 정의되어 있는 것은 전부 다 정의를 해야 한다. 초과되는 것은 상관 없다 미만은 안된다.

인터페이스를 사용한 클래스에서 인터페이스 안 요소를 전부다 다 작성을 해야 한다.
요소를 충족시키면 초과해도 상관없다.

let ori : any = new Dog('오리', 3); //잘 선언된다.

function instanceOfAnmial(object: any) : object is Animal{ //type predicate 사용 
  return 'jump' in object; //객체에서 어떤 키가 있는지 체크할 때에는 in키워드 사용
}

if(instanceOfAnmial(ori)){
  ori // type predicate로 Animal타입으로 유추가 된다.
}

type predicate를 확인하기 위해서 any타입으로 두고 네로잉이 잘되는지 확인해보면 Animal타입으로 유추가 된다.

class Cat implements Animal, Pet{
  //Animal
  name:string;
  age:number;

  // Pet
  legsCount: number;

  constructor(name:string, age:number, legsCount:number){
    this.name = name;
    this.age = age;
    this.legsCount = legsCount;
  }

  //Animal
  jump(): string{
    return `${this.name}이 점프를 합니다.`
  }

  //Pet
  bark(): void{
    console.log("냐옹")
  }
}  
//다중인터페이스도 마찬가지로 모두 다 충족되어야 한다. 초과는 상관없다.


type AnimalAndPet = Animal & Pet;

class Cat2 implements AnimalAndPet{
  //Animal
  name:string;
  age:number;

  // Pet
  legsCount: number;

  constructor(name:string, age:number, legsCount:number){
    this.name = name;
    this.age = age;
    this.legsCount = legsCount;
  }

  //Animal
  jump(): string{
    return `${this.name}이 점프를 합니다.`
  }

  //Pet
  bark(): void{
    console.log("냐옹")
  }
}  
//implement에다가 여러개 콤마로 구분하는 것과 type으로 인터섹션으로 합친 것과 같다.

다중 인터페이스도 마찬가지로 인터페이스에 정의된 요소를 정의해야 한다.
또한 여러개의 인터페이스를 타입화해서 사용하는 것도 된다.

interface WrongInterface1{
  name:string;
}

interface WrongInterface2{
  name:number;
}

// class Person implements WrongInterface1, WrongInterface2{
//   // name:string; // 에러가 생긴다 name은 string과 number로 동시에 만족시키는 타입은 없기 때문에 never타입이 된다.
//   // name:number; //
// }

다중인터페이스는 &의 개념이기 때문에 같은 key가 있으면 동시에 만족시켜야 한다. 해당 타입은 string & number이기 때문에 never타입이 되어 에러가 발생한다.

class Idol {
  name:string;
  age: number;

  constructor(name:string, age:number){ // 생성자 형태를 인터페이스로 구현하고 싶다, 생성자 형태를 가지고 있는 클래스르 타입화 하고 싶다
    this.name = name;
    this.age = age;
  }
}

interface IdolConstructor{          //생성자가 실행이되면 Idol이 반환된다.
  new (name:string, age:number): Idol;  //new키워드를 사용해서 생성자 실행함, 함수를 정의할 때 처럼 똑같이 쓰지만 new만 쓰면된다.
}

function createIdol(constructor: IdolConstructor, name: string, age:number){
  return new constructor(name, age)
}

console.log(createIdol(Idol, '아이유', 32));

상속

class Parent{
  name: string;

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

  dance(){
    console.log("춤을 춥니다.")
  }
}

class Child extends Parent{
  age:number;

  constructor(name:string, age:number){
    super(name);
    this.age = age;
  }

  sing(){
    console.log("노래를 부릅니다.")
  }
}

const codefactory = new Parent('코드팩토리');
const ahri = new Child('아리', 12);

codefactory.dance()
// codefactory.sing()

ahri.dance();
ahri.sing();

let person: Parent;
person = codefactory;
person = ahri; // 자식 클래스이지만 상속을 받으면 부모타입이 될 수 있다. 그래서 가능하다.

let person2: Child;

person2 = ahri;
// person2 = codefactory //부모는 자식 타입이 될 수 없다.

ahri(Child)타입은 person(Parent)타입에 할당이 가능하다.
부모가 자식을 포함하고 있으니 관계적인 측면에서도 가능하다고 생각하면 된다.
반대로
codefactory(Parent)타입은 person2(Child2)타입에 할당하지 못한다.
부모타입은 자식타입에 할당하지 못한다.

class Parent2{
  name : string;

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

class Child2  extends Parent2{
  age? : number;

  constructor(name:string, age?:number){
    super(name);
    this.age = age;
  }
}
const cf2 = new Parent2('코드팩토리');
const ahri2 = new Child2('아리', 20);

let child : Child2;
child = ahri2;
child = cf2; 
// 된다. age가 옵셔널이기 때문이다. age가 없으면 child와 parent구조가 똑같다고 생각한다.

클래스에서 optional을 주의하여 사용해야 한다. 앞서서 부모타입은 자식타입에 할당하지 못한다는 것을 알 수 있었다.
하지만 optional을 사용하면 TS는 child와 parent 구조가 똑같다고 생각하여 가능하게 된다.

오버라이드

class Parent{
  shout(name: string){
    return `${name}아 안녕!`;
  }
}

class WrongChild extends Parent{

  shout(){
    //에러
  }
} // 부모 클래스에 메서드를 자식 클래스에서 덮어쓰기 할 때에는 몇가지 룰이 있다.

class Child extends Parent{
  shout(name: string, me?: string) : string{
    if(!me){
      return super.shout(name)
    }else{
      return super.shout(name) + `내 이름은 ${me}야!`;
    }
  }
}

const child = new Child();
console.log(child.shout('아이유'));
console.log(child.shout('아이유','코드팩토리'));

오버라이드 조건
1) 부모 메서드와 반환 타입이 일치해야 한다.
2) 부모 메서드에 필수인 파라미터들이 존재해야 한다.
3) 부모 메서드에서 optional인 파라미터들이 자식에서 필수로 지정되면 안된다.

class PropertyParent{
  name: string;

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

// class PropertyChild extends PropertyParent{
//   name: number;

//   constructor(name: number){
//     this.name = name;
//   }
// } //에러발생


class PropertyParent2{
  name : string | number;

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

class PropertyChild2 extends PropertyParent2{
  name: string;

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

const child2 = new PropertyChild2('헬로우');
child2.name

속성도 오버라이드 할 수 있는데 조건은 부모 속성이 string | number처럼 범위를 가진 타입이여야 한다.
자식에서 범위 중 타입내에서 오버라이드가 가능하다.

abstract class

Abstract class란? 인스턴스화를 하지 못하게 만드는 것이다.

abstract class ModelWithId{
  id: number;

  constructor(id: number){
    this.id = id;
  }
}
const modelWithId = new ModelWithId(123); 
//에러 발생 abstract 클래스에 인스턴스를 만들 수 없다. 직접적인 선언 불가다

에러가 발생한다.

그럼 언제 Abstract class를 사용하는가?
이 자체로는 인스턴스화 할 일이 절대로 없는데 공유되는 값들을 프로퍼티나 메소드들을 정의하고 싶고 다른 곳에서 상속을 받고 싶을 때 사용한다.

class Product extends ModelWithId{

}

const product = new Product(1); // 가능
product.id // number
abstract class ModelWithAbstractMethod{
  abstract shout(name: string): string; //실제로 body를 입력하지 않고 정의만 입력 가능하다.
  //abstract 메소드를 선언을 하면은 상속을 받았을 때 method의 구현을 강제할 수 있다.
}

class Person extends ModelWithAbstractMethod{ //에러가 발생하는데 shout를 구체화 해야 한다는 에러가 발생한다.
  shout(name: string): string {
    return '소리질러'; // 구체화 하면 에러가 사라진다.
  }
}

visibility_keywords

1) public (기본값) - 어디서든 접근이 가능하다.
2) protected - 현재 클래스 및 하위 (자식) 클래스에서 접근 가능하다.
3) private - 현재 클래스 내부에서만 접근 가능하다.

class PropertyTestParent{
  public publicProperty = 'public property'; // public이 없어도 기본값으로 public이 된다.
  protected protectedProperty = 'protected property';
  private privateProperty = 'private property';
  #jsPrivateProperty = 'js private property'; // JS에 존재하는 문법


  test(){
    this.publicProperty; //접근 가능
    this.protectedProperty //접근 가능
    this.privateProperty; //접근 가능
    this.#jsPrivateProperty; //접근 가능
  }
}

클래스 내부에서 선언한 것은 public, protected, private 다 접근 가능하다.

class PropertyTestChild extends PropertyTestParent{
  test(){
    this.publicProperty; //접근 가능
    this.protectedProperty //접근 가능
    // this.privateProperty; //접근 불가능
    // this.#jsPrivateProperty; //접근 불가능
  }
}

상속받은 클래스에서 접근 가능한 것은 public, protected다.

const instance = new PropertyTestChild();

instance.publicProperty; 
//인스턴스를 만들었을 때 접근 가능한 것은 public이다. 나머지 private,protected 접근 x

인스턴스를 만들었을 때 접근 가능한 것은 public이다. 나머지 private,protected 접근 x

profile
알고리즘 정리 블로그입니다.

0개의 댓글