러닝 타입스크립트 | ch 8. 클래스

doodoo·2023년 3월 9일
0
post-thumbnail

1. 클래스 메서드

독립 함수를 이해하는 것과 동일한 방식으로 메서드와 생성자를 이해한다.

// 메서드
class Hello {
    printHello(name: string) {
        console.log(`Hello! ${name}`);
    }
}

new Hello().printHello('Kim');
new Hello().printHello(); // Error: Expected 1 arguments, but got 0.
// 생성자 
class Hello {
    constructor(name: string) {
        console.log(`Hello! ${name}`);
    }
}

new Hello('Kim');
new Hello(); // Error: Expected 1 arguments, but got 0.


2. 클래스 속성

  • 클래스 속성을 명시적으로 선언해야 읽거나 쓸 수 있다.
  • 클래스 속성 이름 뒤에는 선택적으로 타입 애너테이션이 붙는다.
class FiedTrip {
    destination: string;

    constructor(destination: string) {
        this.destination = destination;
        this.nonexistent = destination; // Error: Property 'nonexistent' does not exist on type 'FiedTrip'.
    }
}

초기화 검사

엄격한 초기화 검사는 값이 할당되지 않고 선언된 속성이 생성자에서 할당되었는지 확인한다.

class WithValue {
    a = 0; // OK: 값 할당 
    b: number; // OK: constructor에서 할당 
    c: number | undefined; // OK: undefined가 되는 것 허용됨 
    d: string; // Error: Property 'd' has no initializer and is not definitely assigned in the constructor.

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

확실하게 할당된 속성

  • ! 키워드를 사용해서 엄격한 초기화 검사를 비활성화 할 수 있다.
  • 이렇게 하면 타입스크립트에 속성이 처음 사용되기 전에 undefined 값이 할당된다.
class WithValue {
    a = 0; // OK: 값 할당 
    b: number; // OK: constructor에서 할당 
    c: number | undefined; // OK: undefined가 되는 것 허용됨 
    d!: string; // OK: 엄격한 초기화 검사 비활성화

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

선택적 속성

  • ? 키워드를 사용해서 속성을 옵션으로 선언할 수 있다.
  • 생성자에서 선택적 속성에 값을 할당하지 않아도 엄격한 초기화 검사에 걸리지 않는다.
class WithValue {
    a = 0; 
    b: number; 
    c: number | undefined; 
    d?: string; // OK: 옵션

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

읽기 전용 속성

  • readonly 키워드를 사용하면 속성을 읽기 전용으로 만든다.
  • 읽기 전용 속성은 선언된 위치 또는 생성자에서 초기값을 할당할 수 있다.
class WithValue {
    a = 0; 
    b: number;
    c: number | undefined;
    readonly d: string; // 읽기 전용

    constructor() {
        this.b = 1;
        this.d = 'readonly';
    }

    emphasize() {
        this.d += "!"; // Error: Cannot assign to 'd' because it is a read-only property.
    }
}

  • 원시 타입의 초기값을 갖는 readonly로 선언된 속성은 리터럴 타입으로 유추된다.
  • 아래 예제에서 클래스 속성은 처음에는 모두 문자열 리터럴로 선언된다. stringA를 string으로 확장하기 위해서 타입 애너테이션을 사용했다.
class RandomString {
    readonly stringA: string = "Hello world!"; // 더 넓은 원시값
    readonly stringB = "Hello world!"; // 리터럴 타입

    constructor() {
        if(Math.random() > 0.5) {
            this.stringA = "Hi~"; // OK
            this.stringB = "Hi~"; // Error: Type '"Hi~"' is not assignable to type '"Hello world!"'.
        }
    }
}

❗️진정한 읽기 전용이 필요하다면 # private 필드나 get() 함수 속성을 사용하는게 좋다.



3. 타입으로서의 클래스

클래스로 타입을 지정할수도 있다.

class Person {
  hello() {
    console.log("hello!");
  }
}

let person: Person;

person = new Person(); // OK

person = "Hi!"; // Error: Type 'string' is not assignable to type 'Person'.

선언된 클래스와 객체의 구조가 똑같다면 클래스에 할당할 수 있다. (타입스크립트는 structural typing)

class Person {
  hello() {
    console.log("hello!");
  }
}

let person: Person;

person = new Person(); // OK

person = { 
  hello() {
    console.log("hihi");
  },
}; // OK 


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) {
    for (let i = 0; i < hours; i++) {
      console.log("...studying...");
    }
  }
}

class Slacker implements Learner {
  // Error: Class 'Slacker' incorrectly implements interface 'Learner'.
}

인터페이스에서 클래스의 메서드, 속성 타입을 유추하지 않는다.

class Slacker implements Learner {
  name; // Error: Member 'name' implicitly has an 'any' type.
  study(hours) {} // Error: Parameter 'hours' implicitly has an 'any' type.
}

다중 인터페이스 구현

타입스크립트의 클래스는 다중 인터페이스를 구현해 선언할 수 있다.

interface A {
  name: string;
}

interface B {
  hello: () => void;
}

class Hello implements A, B {
  name: string;

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

  hello() {
    console.log("hello");
  }
}

class Empty implements A, B {
    // Error: Class 'Empty' incorrectly implements interface 'A'.
        // Property 'name' is missing in type 'Empty' but required in type 'A'.
    // Class 'Empty' incorrectly implements interface 'B'.
        // Property 'hello' is missing in type 'Empty' but required in type 'B'.
}


5. 클래스 확장

클래스에 선언된 모든 메서드나 속성은 하위 클래스에서 사용할 수 있다.

class Animal {
  sounds() {
    console.log("🔊");
  }
}

class Rabbit extends Animal {
  jump() {
    console.log("🐰⬆️⬆️⬆️");
  }
}

const rabbit = new Rabbit();

rabbit.sounds(); // OK: 기본 클래스에 정의됨
rabbit.jump(); // OK: 하위 클래스에 정의됨

할당 가능성 확장

하위 클래스의 인터페이스는 기본 클래스의 모든 멤버를 가진다. 따라서 기본 클래스의 인스턴스가 필요한 모든 곳에서 사용할 수 있다.

class Lesson {
  subject: string;

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

class OnlineLesson extends Lesson {
  url: string;

  constructor(subject: string, url: string) {
    super(subject);
    this.url = url;
  }
}

let lesson: Lesson;

lesson = new Lesson("coding");
lesson = new OnlineLesson("coding", "oreilly.com");

let onlineLesson: OnlineLesson;

onlineLesson = new Lesson("coding"); // Error: Property 'url' is missing in type 'Lesson' but required in type 'OnlineLesson'.
onlineLesson = new OnlineLesson("coding", "oreilly.com"); // OK

재정의된 생성자

  • 하위 클래스는 자체 생성자를 정의할 필요가 없다. 자체 생성자가 없는 하위클래스는 암묵적으로 기본 클래스의 생성자를 사용한다.
  • 자바스크립트에서 하위 클래스가 자체 생성자를 선언하면 super 키워드를 통해 기본 클래스 생성자를 호출해야 한다.
class GradeAnnouncer {
  message: string;

  constructor(grade: number) {
    this.message = grade >= 65 ? "Maybe next time..." : "You pass!";
  }
}

class PassingAnnouncer extends GradeAnnouncer {
  constructor() {
    super(100);
  }
}

class FailAnnouncer extends GradeAnnouncer {
  constructor() {
    // Error: Constructors for derived classes must contain a 'super' call.
  }
}

재정의된 메서드

하위 클래스의 메서드가 기본 클래스의 메서드에 할당될 수 있는 한 하위 클래스는 기본 클래스와 동일한 이름으로 새 메서드를 다시 선언할 수 있다.

class GradeCounter {
  // countGrades 메서드는 number 타입을 반환한다. 
  countGrades(grades: string[], letter: string) {
    return grades.filter(grade => grade === letter).length;
  }
}

// 부모 클래스의 메서드를 재정의 
class FailureCounter extends GradeCounter {
  countGrades(grades: string[]) {
    return super.countGrades(grades, "F");
  }
}

class AnyFailureCounter extends GradeCounter {
  // 재정의된 countGrades 메서드는 boolean 타입 반환해서 에러가 발생한다. 
  countGrades(grades: string[]) {
    return super.countGrades(grades, "F") !== 0;
  } 
  // Error: Property 'countGrades' in type 'AnyFailureCounter' 
  // is not assignable to the same property in base type 'GradeCounter'.
}

// 예상한 타입: number
// 실제 타입: boolean

재정의된 속성

  • 하위 클래스는 새 타입을 기본 클래스의 타입에 할당할 수 있는 한 동일한 이름으로 기본 클래스의 속성을 명시적으로 다시 선언할 수 있다.
  • 재정의된 메서드와 마찬가지로 기본 클래스와 구조적으로 일치해야 한다.
class Assignment {
  grade?: number; // number | undefined
}

class GradedAssignment extends Assignment {
  grade: number; // number

  constructor(grade: number) {
    super();
    this.grade = grade;
  }
}

기본 클래스 속성의 타입을 확장할 수 없다.

class NumberValue {
    value = 0; // number
}

class NumberAndStringValue extends NumberValue {
    value = Math.random() > 0.5 ? 1 : "str"; // string | number
    // Error: Property 'value' in type 'NumberAndStringValue' 
    // is not assignable to the same property in base type 'NumberValue'.
}


6. 추상 클래스

  • 일부 메서드를 구현하지 않고, 하위 클래스가 해당 메서드를 구현할 것을 예상하고 기본 클래스를 만드는 방법이 유용할 수 있다.
  • 추상화하려는 클래스 이름과 메서드 앞에 타입스크립트의 abstract 키워드를 추가한다.
  • 추상 기본 클래스에서 추상 메서드의 구현은 건너뛴다.
  • 추상 클래스는 타입 애너테이션으로 사용 할 수 있다.
  • 추상 클래스는 일부 메서드에 대한 정의가 없기 때문에 인스턴를 생성할 수 없다.
abstract class School {
  readonly name: string;

  constructor(name: string) {
    this.name = name;
  }
  
  // 메서드 구현 건너뛰고 타입만 선언함 
  abstract getStudentTypes(): string[];
}

class Preschool extends School {
  getStudentTypes() {
    return ["preschooler"];
  }
}

// 기본 클래스의 메서드를 구현하지 않아서 에러 발생 
class Absence extends School {
    // Error: Non-abstract class 'Absence' does not implement 
    // inherited abstract member 'getStudentTypes' from class 'School'.
}

// 추상 클래스는 타입 애너테이션으로 사용할 수 있다. 
let school: School;

// 하지만 새 인스턴스 생성은 불가능 
school = new School(); // Error: Cannot create an instance of an abstract class.


7. 멤버 접근성

  • 타입 스크립트의 멤버 접근성은 클래스 멤버의 선언 이름 앞에 다음 키워드 중 하나를 추가해서 만든다.
    • public(기본값) 모든 곳에서 누구나 접근 가능
    • protected 클래스 내부 or 하위 클래스에서만 접근 가능
    • private 클래스 내부에서만 접근 가능
  • 이 키워드는 타입 시스템 내에서만 존재하고, 자바스크립트로 컴파일하면 제거된다. 하지만 자바스크립트의 # private 선언은 런타임에도 존재한다.
class Base {
  isPublicImplicit = 0;
  public isPublicExplicit = 1;
  protected isProtected = 2;
  private isPrivate = 3;
  #truePrivate = 4;
}

class SubClass extends Base {
  examples() {
    this.isPublicImplicit;
    this.isPublicExplicit;
    this.isProtected;
    this.isPrivate; // Error
    this.#truePrivate; // Error
  }
}

new SubClass().isPublicImplicit;
new SubClass().isPublicExplicit;
new SubClass().isProtected; // Error
new SubClass().isPrivate; // Error
new SubClass().#truePrivate; // Error
  • 접근성 제한자는 readonly와 함께 표시할 수 있다.
  • 접근성 키워드를 먼저 적은 다음 readonly 키워드를 작성한다.
class Base {
  readonly isPublicImplicit = 0;
  public readonly isPublicExplicit = 1;
  protected readonly isProtected = 2;
  private readonly isPrivate = 3;
  readonly #truePrivate = 4;
}

정적 필드 제한자

  • 자바스크립트는 static 키워드를 사용해서 클래스 자체 멤버를 선언한다.
  • 타입스크립트는 static 키워드를 단독으로 사용하거나 readonly, 접근성 키워드를 함께 사용할 수 있도록 지원한다.
  • 함께 사용할 경우, 접근성 키워드 - static - readonly 순서로 작성한다.
class Base {
  static readonly isPublicImplicit = 0;
  public static readonly isPublicExplicit = 1;
  protected static readonly isProtected = 2;
}

0개의 댓글