관리 메뉴

꿈꾸는 개발자

8장 클래스 본문

Learning Typescript

8장 클래스

rickysin 2023. 3. 10. 16:17

8.1 클래스 메서드


  • 매개변수 기본 타입: any
  • 독립 함수와 동일한 방식으로 이해
  • 클래스 생성자(constructor) 또한 동일한 취급
//메서드
class Greeter{
    greet(name:string){
        console.log(`${name}`);
    }
}
new Greeter().greet('haha'); //ok
new Greeter().greet(); //Error: 인수 필요!

=================================================
//생성자
class Greeter{
   constructor(mess:string){
    console.log(`${mess}`);
   }
}
new Greeter("haha"); 
new Greeter() //Error 매개변수 제공 안 해서

8.2 클래스 속성


  • TS 클래스 속성 읽기/쓰기 하려면 명시적으로 선언
  • 속성 뒤 선택적 타입 애너테이션 가능
  • TS도 생성자 내의 할당에 대해 그 멤버 클래스에 존재하는 멤버인지 추론X ⇒ 의문(추론하지 않는다면서 nonexistent는 왜 에러)?
class FieldTrip{
    //멤버 변수 => 멤버 변수가 없다면 this.des=des도 에러
    des:string;
    constructor(des:string){
        this.des=des;
        this.nonexistent=des; //type FieldTrip에 존재X
				//클래스 속성 nonexistent를 선언X => 할당 허용X
    }
}

===========================================

class FieldTrip{
    //멤버 변수로만 선언해도 에러 사라짐
    des:string;
    nonexistent:string;
    constructor(des:string){
        this.des=des;
        this.nonexistent=des; 
    }
}

8.2.1 함수 속성(???)


  • 두 가지 함수 선언 방식 (JS)
    • myFunction(){} 형식 ⇒ 클래스 prototype에 할당(인스턴스 당 동일한 함수 정의 사용)
    • 함수 속성 선언 방식 ⇒ 인스턴스 당 새로운 함수 생성
    • 함수 속성: 독립 함수와 동일한 구문 사용 가능!
    class WithPropParam{
        takesParam=(input:boolean)=> input? "Yes":"No";
    }
    const instance=new WithPropParam();
    
    instance.takesParam(true);
    instance.takesParam(123); //Error type 에러 발생
    
    8.2.2 초기화 검사
  • strict compiler environment (undefined 체크)
  • 기본적으로 값을 초기화하지 않으면 undefined이 됨!
class Value{
    a=0; //즉시 할당 가능
    b:number; //constructor()에서 할당해야 함
    c:number|undefined; 
    d:number; //여기만 에러 발생 strict compile환경에서 undefined이 들어가 있어서
    constructor(){
        this.b=1;
    }
}

  • strictNullcheck을 해제하면 에러 사라짐 ⇒ 이럴 경우 JS 런타임에서 에러가 발생할 수 있음 ().property.length의 경우 (undefined의 length 측정 불가) ⇒ BUT, TS에선 문제 없이 넘어갈 것이다.

확실하게 할당된 속성

  • 엄격한 속성이 적용 안되는 속성인 경우 이름 뒤에 “!”을 추가!(검사 비활성화) ⇒undefined 할당
  • ! 어서션을 사용하는 대신 리펙터링을 통해 코드 개선을 추천

8.2.3 선택적 속성


class MissingInitial{
    property?:string; //strict 검사 적용X
}
new MissingInitial().property?.length //ok;
new MissingInitial().property.length //Error: Object is possible undefined

8.2.4 읽기 전용 속성


  • 속성 이름 앞에 readonly를 추가해 속성을 읽기 전용으로 선언 ⇒ 컴파일 후 삭제
  • 선언된 위치에서 초깃값/생성자에서 값만 할당 가능 그 외는 불가!
class ReadOnly{
    readonly text:string;
    constructor(){
        this.text="";
    }
}
  • 진정한 읽기 전용을 실천하기 위해선 #private/get()함수 속성을 고려해라!
  • 원식 타입을 갖는 readonly 속성 ⇒ 리터럴 타입으로 유추됨
class Random{
    readonly explicit:string="aaaaa";
    readonly implicit="bbbbb" //더 구체적인 값(리터럴 값)

    constructor(){
        if(Math.random()>0.5){
            this.explicit="cccc";
            this.implicit="dddd";//에러 발생 리터럴 값이기 때문에
        }
    }
}
const quote=new Random();
quote.explicit;//타입 string 
quote.implicit;//"bbbbb"

8.3 타입으로서의 클래스


  • TS ⇒ 클래스 선언(런타임 값) + 타입 애너테이션 모두 생성 (정확히 무슨 의미)
class Teacher{
    sayHello(){
        console.log("aaaaa");
    }
}
let teacher:Teacher;
teacher=new Teacher();//ok
teacher="AAA"; //Error: string => type Teacher assign불가
  • TS는 클랫의 동일한 멤버를 모두 포함하는 모든 객체 타입을 클래스에 할당할 수 있는 것으로 간주 ⇒ 구조적 타이핑 방식X(객체 형태만 고려하기 때문) ⇒구조적 타이핑도 객체를 고려하는 것 아님??집합의 관계를 통해?
class Bus{
    getAb(){
        return ["a","b "];
    }
}
//클래스를 매개변수로 받음
function withBus(bus:Bus){
    console.log(bus.getAb());
}
withBus(new Bus);

withBus({
    getAb:()=>["c"]
})
withBus({
    getAb:()=>123,//Type number =>string[] 할당 불가
})
  • 대부분 클래스 타입 요청 위치에 객체 값 전달X

8.4 클래스와 인터페이스


  • 클래스 뒤에 implements 키워드 + 인터페이스 이름 클래스의 인스턴스가 추가한 인터페이스를 준수한다고 선언할 수 있음
interface Learner{
    name:string; 
    study(hours:number):void;
}
class Student implements Learner{
    name:string;
    constructor(name:string){
        this.name=name;
    }
    study(hours:number){ //만약 타입 애너테이션을 하지 않으면 any으로 간주함 
        for(let i=0;i<hours;i++){
            console.log("studying");
        }
    }
}
class slacker implements Learner{
    //property를 선언하지 않아 에러 발생
}
  • 인터페이스 멤버를 함수로 선언하기 위해 메서드 구문 사용
  • 인터페이스 구현 목적: 안정성 때문 ⇒ 타입 검사기로 신호를 보냄!

8.4.1 다중 인터페이스 구현


  • TS 클래스는 다중 인터페이스 구현 ⇒ 선언 가능
interface Graded{
    grades:number[];
}

interface Reporter{
    report:()=>string;
}
class ReprotCard implements Graded, Reporter{
    grades:number[];
    constructor(grades:number[]){
        this.grades=grades;
    }

    report(){
        return this.grades.join(",");
    }
}
class Empty implements Graded,Reporter {}
//Error: property grades 및 report를 작성하지 않아 에러 발생!
  • 출동하는 인터페이스를 구현하는 클래스 선언하면 ⇒ 타입 오루 발생

8.5 클래스 확장


  • TS 기존 JS 개념에 타입 검사를 추가함
    • 기존 클래스의 메서드 + 속성 ⇒ 파생 클래스에서 사용이 가능
    • 없는 속성/메서드 접근시 에러 발생

8.5.1 할당 가능성 확장


class A{
    propA:string;
    constructor(propA:string){
        this.propA=propA;
    }
}

class B extends A{
    B:string;
    constructor(A:string,B:string){
        super(A);
        this.B=B;
    }
}

let a:A;
a=new A("coding"); //당연히 ok
a=new B("coding","haha"); //ok

let b:B;
b=new B("coding","haha") //ok
b= new A("coding") //Error:property "B" is missing type in A
  • JAVA/C++과 동일한 작동 원리 ⇒ 집합 관계로 이해
  • 하지만, TS에선 추가적으로 구조적 타이핑으로 인해 하위 클래스의 모든 멤버가 동일하다면 ⇒ 기본 클래스 인스턴스를 사용할 수 있다.
  • 자주 발생하지는 않음!

8.5.2 재정의된 생성자


  • JS처럼 하위 클래스는 자체 생성자 정의 필요X ⇒암묵적으로 기본 클래스 생성자 사용
  • 생성할 경우: ⇒ super를 통한 기본 클래스 생성자 호출(이 때 타입 검사기는 매개변수가 올바르게 선언됐는지 확인)
  • 하위 클래스: 기본 클래스와 상관없이 모든 매개변수 선언 가능!
class A{
    propA:string; 
    constructor(grade:number){
        this.propA=grade>=65?"aaa":"bbbb";
    }
}
class B extends A{
    constructor(){{ //올바르게 선언하면 문제X
        super(100);
    }}
}
class C extends A{
    constructor(){} //기본 생성자를 호출하지 않으면 오류 발생
}
  • 생성자에서 this를 사용하기 전 반드시 super()를 호출해야 함 ⇒ this를 먼저 사용할 경우 에러 발생!

8.5.3 재정의된 메서드


  • 하위 클래스의 메서드가 기본 클래스의 메서드에 할당될 수 있는 경우 ⇒ 동일한 이름으로 새 메서드 재선언 가능(포함 관계이기 때문에 메서드 타입도 기본 메서드 대신 사용할 수 있어야 함)
    • 다른 언어처럼 매개변수 추가 등의 Overriding은 허용하지 않는 것인가?
      • https://www.typescriptlang.org/docs/handbook/2/classes.html (공식문서에 있는 Overriding Methods 부분 참고)
      • 밑 코드를 살펴보면, class B는 A를 상속 받고, 내부 methodA에 새로운 매개변수인 name?:string이 추가됨을 볼 수 있다 ⇒ 이럴 경우 TS 자체에선 Base class의 구조가 어긋나지 않았다고 판단 매개변수의 추가를 허용해준다.
      • 하지만 ?이 사라지는 순간 에러가 발생함!
      • Overloading 자체는 허용하지 않는듯?(검색해도 잘 안 나옴)
class A{
    methodA(grades:string[],letter:string){
        return grades.filter(grade=>grade===letter).length;
    }
}
class B extends A{
    methodA(grades:string[],letter:string,name?:string){ //선택 매개변수일 경우 사용이 가능한듯!
        if(name===undefined){
        return super.methodA(grades,"f");
        }else{
        console.log(name);
        return super.methodA(grades,"f");
        }
    
    }
}
class C extends A{
    methodA(grades:string[]){ //Error; 기본 클래스(A)의 반환값이 number type인데 
                                //boolean typed을 반환해서 에러 발생
        return super.methodA(grades,"F")!==0;
    }
}
const counter:A=new C();//애초에 C가 에러이기 때문에 에러 발생
//에상 타입:number;
//실제 타입: boolean;
const count =counter.methodA(["A","B","C"])

8.5.4 재정의된 속성


  • 위와 마찬가지로 속성에도 동일한 원리가 적용된다. 메서드와 마찬가지로 기본 클래스와 구조적으로 일치해야 함
  • 보통 하위 클래스에선 해당 속성을 유니언 타입의 더 구체적인 하위 집합/클래스 속성 타입에서 확장되는 타입으로 만듦⇒ 확장된 타입이란 의미가 모호함(밑의 경우 class B에 grade: number | string으로 할 경우 에러가 발생함(무조건 기본 클래스를 따라야 하는 것처럼 보이는데??) ⇒ 네로잉의 경우는 문제 없어 보임! (다른 언어의 overriding의 개념과의 차이점을 분석해 볼 것!)
class A{
    grade?:number; //number | undefined으로 선언? 이것과는 다르지 않나??
}
class B extends A{
    grade:number; 
    constructor(grade:number){
        super();
        this.grade=grade; //여기에선 항상 존재하는 number type으로 선언함!
    }
}
  • 속성 유니언 타입의 허용된 값 집합을 확장 할 수 없음 ⇒ 할 경우 하위 클래스 속성은 기본 클래스 속성 타입에 할당 불가!
class A{
    value=0;
}
class B extends A{
    value=Math.random()>0.5? 1: "...."; //TypeError: type number | string X =>to type number
}
cosnt instance:A=new B();
//예상 한 타입: number
//실제 타입: number | string
instance.value;

8.6 추상 클래스


  • 하위 클래스가 일부 메서드를 만들 것을 예상하고 기본 클래스를 구현하는 방법도 있음
  • abstract 키워드를 class명 앞에 추가함! ⇒ 인터페이스와 동일한 방식으로 선언됨!
abstract class A{
    readonly name:string;
    constructor(name:string){
        this.name=name;
    }
    abstract getType():string[];
}
class B extends A{
    getType(){
        return ["AAA"];
    }
}
class C extends A {} //추상 클래스를 상속했으면 무조건 메서드를 선언/구현해야 함!
  • 추상화 클래스는 직접 인스턴스 생성 불가X
  • 추상화 클래스의 경우 세부 사항이 채워질 것이란 예상되는 프레임워크에 자주 사용됨

8.7 멤버 접근성


  • JS에선 멤버 이름에 #을 추가해서 private 클래스임을 나타냄 (프라이버시 강화)
  • TS는 타입 시스템에만 존재하는 클래스 메서드와 속성에 대해 조금 더 미묘한 프라이버시 정의 집합을 허용함 (클래스 멤버의 선언 이름 앞에 키워드를 추가함)⇒ 컴파일 되면 사라짐(private만 런타임에도 존재함)
    • public:(기본값) 모든 곳 누구나 접근 가능
    • protected: 클래스 내부 또는 하위 클래스에서만 접근 가능
    • private: 클래스 내부에서만 접근 가능
class Base{
    isA=0;
    public a=1; 
    protected b=2; 
    private c=3 
    #truePrivate=4;
}
class Subclass extends Base{
    exmaples(){
        //접근 가능한 멤버 속성들
        this.isA;
        this.a
        this.b
    }
    
    //접근 불가능 한 멤버 속성
    this.c;

    this.#truePrivate;
}

//protected로 선언돼서 외부 접근 불가 Base + subclass 내부에서만 접근 가능
new Subclass().b

//private은 당연히 안됨
new Subclass().c

<aside> 💡 private readonly name: string 형태와 같이 사용할 수 있음

</aside>

8.7.1 정적 필드 제한자


  • JS에선 static 키워드를 사용해 클래스 자체에 멤버를 선언함
    • static (단독)/ + readonly 같이 사용 가능
      • 순서: 접근성 키워드 ⇒ static ⇒ readonly
class A{
    protected static readonly a:"base";
    protected static readonly prompt="asdfasfd";

    guess(getAnswer:(prompt:string)=>string){
        const answer=getAnswer(A.prompt);
    }
    if(answer===A.a){
        console.log("yes");
    }else{
        console.log("No");
    }
}

A.a //protected라서 안됨!

Reference:

  • 조시 골드버그, 러닝 타입스크립트, 고승원 옮김, 2023
  • Typescript Handbook