[하루 30분 러닝 타입스크립트] 클래스(1)

✨ 클래스 메서드

class Greeter {
    message: string

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

    greet(name: string){
        console.log(`${this.message} ${name}`)
    }
}

new Greeter("hi").greet("Jack") // OK
new Greeter().greet("Jack") // ERROR Expected 1 arguments, but got 0.
new Greeter("hi").greet() // ERROR Expected 1 arguments, but got 0.

✨ 클래스 속성

클래스 속성을 읽거나 쓰려면 명시적으로 선언해야 합니다.

인스턴스에 존재하지 않는 멤버에 접근하려 하면 오류를 발생시킵니다.

class Greeter {
    message: string

    constructor(message: string, name: string){
        this.message = message;
        this.name = name // ERROR Property 'name' does not exist on type 'Greeter'.
    }
}

함수 속성

자바스크립트는 다음과 같은 특징을 가집니다.

메서드 접근 방식은 프로토타입에 할당하여 모든 클래스 인스턴스는 동일한 함수 정의를 사용합니다.

class MyMethodAndProperty {
    myMethod () {}
    myProperty = () => {}
}

console.log(new MyMethodAndProperty().myMethod === new MyMethodAndProperty().myMethod)
// true
console.log(new MyMethodAndProperty().myProperty === new MyMethodAndProperty().myProperty)
// false

초기화 검사

엄격한 컴파일러 설정이 활성화 된 상태에서는 다음과 같은 특징을 가집니다.

사용되지 않는 속성은 타입 오류로 반환됩니다.

class WithValue {
    immediate = 0;
    later: number;
    mayBeUndefined: number | undefined;
    unused: string; // ERROR Property 'unused' has no initializer and is not definitely assigned in the constructor.

    constructor(){
        this.later = 1;
    }
}

엄격한 컴파일 설정이 되지 않는다면, 다음은 컴파일이 됩니다.

하지만 자바스크립트 런타임 과정에서 오류가 발생하게 됩니다.

class MissingInitializer {
    property: string;
}

new MissingInitializer().property.length;

다음과 같은 상황에서는 엄격한 초기화 검사를 의도적으로 할당하지 않을 수 있습니다.

! 어서션을 이용하여 검사를 비활성화 할 수 있습니다.

하지만 이는 안정성을 줄이는 행위이므로, 리팩터링이 필요하다는 신호입니다.

class Queue{
    pending!: string[]; // OK

    initialize(pending: string[]) {
        this.pending = pending
    }

    next() {
        return this.pending.pop();
    }
}

선택적 속성

? 키워드를 사용하여 선택적 속성을 사용할 수 있습니다.

class MissingInitializer {
    property?: string;
}

new MissingInitializer().property?.length // OK
new MissingInitializer().property.length // ERROR Object is possibly 'undefined'.

읽기 전용 속성

readonly 제한자를 사용하여 읽기 전용 속성을 만들 수 있습니다.

하지만 외부인이 readonly 제한자를 존중하지 않을 수 있습니다. 특히 JS를 작성 중인 상황처럼 타입 검사를 하지 않는 사용자라면 더욱 그렇습니다. 따라서 # pribate필드 혹은 get() 함수 속성 사용을 고려하세요.

class Quote {
    readonly text: string;

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

    emphasize(){
        this.text += "1" 
				// ERROR Cannot assign to 'text' because it is a read-only property.
    }
}

타입 스크립트는 값이 나중에 변경되지 않는다는 것을 알기 때문에 더 공격적인 초기 타입 내로잉을 편하게 느낍니다. 아래의 implicit 속성은 타입이 “ABCD”로 내로잉 되었습니다.

class Quote {
    readonly explicit: string = "ABC"
    readonly implicit = "ABCD"

    constructor(){
        if(Math.random() > 0.5) {
            this.explicit = "DEFG" // OK
            this.implicit = "EFG" // ERROR Type '"EFG"' is not assignable to type '"ABCD"'.
        }
    }
}

const quote = new Quote();
quote.explicit; // (property) Quote.explicit: string
quote.implicit; // (property) Quote.implicit: "ABCD"

✨ 타입으로서의 클래스

타입 시스템에서의 클래스는 런타인 값과 타입 애너테이션으로 사용할 수 있는 타입을 모두 생성한다는 점에서 독특합니다

class Teacher {
    sayHello() {
        console.log("Hi")
    }
}

let teacher: Teacher
teacher = new Teacher(); // OK
teacher = "ABC" // ERROR Type 'string' is not assignable to type 'Teacher'.

타입스크립트의 구조적 타이핑이 선언되는 방식이 아닌, 객체의 형태만을 고려하기 때문에 다음과 같은 형태 역시 가질 수 있습니다.

class Teacher {
    sayHello() {
        console.log("Hi")
    }
}

let teacher: Teacher
const obj = {
    sayHello : () => console.log("it's not class")
}
teacher = obj // OK

✨ 클래스와 인터페이스

implements 키워드를 통해 클래스의 해당 인스턴스가 인터페이스를 준수해야함을 선언할 수 있습니다.

interface Learner {
    name: string;
    study(hours: number): void;
}

class Student implements Learner {
    name: string;

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

    study(hours: number) {
        for (let i = 0; i < hours; i += 1) {
            console.log("studing...")
        }
    }
}

타입 애너테이션을 지정하지 않는 한 매개변수를 암묵적으로 any로 간주하기 때문에 타입 오류를 발생합니다.

interface Learner {
    name: string;
    study(hours: number): void;
}

class Student implements Learner {
    name: string;

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

    study(hours) { // ERROR Parameter 'hours' implicitly has an 'any' type.
        for (let i = 0; i < hours; i += 1) {
            console.log("studing...")
        }
    }
}

다중 인터페이스 구현

interface Graded {
    grades: number[];
}
interface Reporter {
    report: () => string;
}

class ReportCard implements Graded, Reporter {
    grades: number[];

    constructor(grades: number[]){
        this.grades = grades
    }

    report() {
        return this.grades.join(",")
    }
}

충돌하는 인터페이스를 구현하면 타입 오류가 발생합니다.

interface Graded {
    grades: number[];
}
interface Graded2 {
    grades: () => number[];
}

class ReportCard implements Graded, Graded2 {
    grades: number[]
    /* ERROR
    Property 'grades' in type 'ReportCard' is not assignable to the same property in base type 'Graded2'.
    Type 'number[]' is not assignable to type '() => number[]'.
    Type 'number[]' provides no match for the signature '(): number[]'.
     */
}