TypeScript - Learning TypeScript Chp. 8 Class

이소라·2023년 4월 15일
0

TypeScript

목록 보기
22/28

8.1 Class Methods

  • 클래스 메서드의 매개변수는 타입이나 기본값을 지정하지 않으면 any 타입을 기본으로 가집니다.
  • 클래스 메서드를 호출하려면, 허용 가능한 수의 인수가 필요합니다.
  • 클래스 메서드는 재귀 함수가 아니라면 대부분의 반환 타입을 유추할 수 있습니다.
class Greeter {
  greet(name: string) {
    console.log(`${name}, do your stuff!`);
  }
}
// Ok
new Greeter().greet('Miss Frizzle');
// Error: Expected 1 arguments, but got 0
new Greeter().greet();
  • TypeScript는 클래스 메서드 호출 시 올바른 타입의 인수가 올바른 개수로 제공되는지 확인하기 위해 타입 검사를 수행합니다.
class Greeted {
  constructor(message: string) {
    console.log(`As I always say: ${message}`);
  }
}

new Greeted('take chances, make mistakes, get messy');
// Error: Expected 1 arguments, but got 0
new Greeted();



8.2 Class Properties

  • TypeScript에서 클래스의 속성을 읽거나 쓰려면 클래스에 명시적으로 선언해야 합니다.
    • 클래스 속성 뒤에는 선택적으로 type annotation이 붙습니다.
class FieldTrip {
  destination: string;
  
  constructor(destination: string) {
    this.destination = destination;
    console.log(`We're going to ${this.destination}`);
    // Error: Property 'nonexistent' does not exist on type 'FieldTrip'
    this.nonexistent = destination;
  }
}
  • 클래스 속성을 명시적으로 선언하면 TypeScript는 클래스 인스턴스에서 무엇이 허용되고, 무엇이 허용되지 않는지 빠르게 이해할 수 있습니다.
const trip = new FieldTrip('planetarium');
// Ok
trip.destination;
// Error: Property 'nonexistent' does not exist on type 'FieldTrip'
trip.nonexistent;

8.2.1 Function Properties

  • JavaScript에는 클래스 멤버를 호출 가능한 함수로 선언하는 2 가지 구문이 있습니다.
    1. 메서드 접근 방식
      • 멤버 이름 뒤에 괄호를 붙입니다.
      • 함수를 클래스 프로토타입에 할당하므로 모든 클래스 인스턴스는 동일한 함수 정의를 사용합니다.
    2. 값이 함수인 속성을 선언하는 방식
      • 클래스의 인스턴스 당 새로운 함수가 생성됩니다.
      • 항상 클래스 인스턴스를 가리켜야 하는 화살표 함수에서 this 스코프를 사용하면, 클래스 인스턴스당 새로운 함수를 생성하는 시간과 메모리 비용 측면에서 유용할 수 있습니다.
class WithMethod {
  // 메서드 접근 방식
  myMethod() {}
}

new WithMethod().myMethod === new WithMethod().myMethod; // true


class WithProperty {
  // 값이 속성인 함수
  myProperty: () => {}
}

new WithProperty().myProperty === new WithProperty().myProperty // false
  • 함수 속성에는 클래스 메서드와 독립 함수의 동일한 구문을 사용하여 매개변수와 반환타입을 지정할 수 있습니다.
    • 함수 속성은 클래스 멤버에 할당된 값이고, 그 값이 함수입니다.
class WithPropertyParameters {
  takesParameters = (input: boolean) => input ? 'Yes' : 'No';
}

const instance = new WithPropertyParametes();
// Ok
instance.takesParameters(true);
// Error: Argument of type 'number' is not assignable to parameter of type 'boolean'
instance.takesParameters(123);

8.2.2 Initialization Checking

  • 엄격한 컴파일러 설정이 활성된 상태에서 TypeScript는 undefined 타입으로 선언된 각 속성이 생성자에서 할당되었는지 확인합니다.
    • 엄격한 초기화 검사는 클래스 속성에 값을 할당하지 않는 실수를 예방할 수 있어 유용합니다.
class WithValue {
  immediate = 0; // Ok
  later: numebr; // Ok (constructor에서 할당)
  mayBeUndefined: number | undefined; // Ok (undefined가 되는 것이 허용됨)
  unused: number; // Error: Property 'ununsed' has no initializer
  // and is not definitely assigned in the contructor
  constructor() {
    this.later = 1;
  }
}
  • 엄격한 초기화 검사가 없다면, 클래스 인스턴스는 undefined 값에 접근할 수 있기 때문에 런라임 시 문제가 발생할 수 있습니다.
class MissingInitializer {
  property: string;
}
// (런타임시) TypeError: Cannot read property 'length' of undefined 
new MissingInitializer().property.length;

! assertion

  • 엄격한 초기화 검사를 적용하면 안되는 속성인 경우에는 이름 뒤에 !를 추가해 검사를 비활성화하도록 설정합니다.
    • 이렇게 하면 TypeScript에 속성이 처음 사용되기 전에 undefined 값이 할당됩니다.
class ActivitiesQueue {
  // pending 속성이 initialize 메서드에 의해 생성자와 별도로 여러 번 초기화될 수 있기 때문에 ! assertion을 추가함
  pending!: string[];
  
  initialize(pending: string[]) {
    this.pending = pending;
  }
  
  next() {
    return this.pending.pop();
  }
}

const activities = new ActivitiesQueue();

acitivities.initialze(['eat', 'sleep', 'learn']);
activivities.next();

8.2.3 Optional Properties

  • 클래스는 선언된 속성 이름 뒤에 ?를 추가해 선택적 속성을 선언할 수 있습니다.
    • 선택적 속성은 | undefined를 포함하는 union 타입과 거의 동일하게 작동합니다.
    • 엄격한 초기화 검사는 생성자에서 선택적 속성을 명시적으로 설정하지 않아도 문제가 되지 않습니다.
class MissingInitializer {
  property?: string;
}
// Ok
new MissingInitializer().property?.length;
// Error: Object is possibly 'undefined'
new MissingInitializer().property.length;

8.2.4 Read-Only Properties

  • 클래스도 선언된 속성 이름 앞에 readonly 키워드를 추가해 일기 전용 속성을 선언할 수 있습니다.
    • readonly 키워드는 타입 시스템에만 존재하며 JavaScript로 컴파일할 때 삭제됩니다.
    • readonly 키워드로 선언된 속성은 선언된 위치 또는 생성자에서 초깃값만 할당할 수 있습니다.
    • 클래스 내의 메서드를 포함한 다른 모든 위치에서 일기 전용 속성은 읽을 수만 있고, 쓸수는 없습니다.
class Quote {
  readonly text: string;
  
  constructor(text: string) {
    this.text = text;
  }
  
  emphasize() {
    // Error: Cannot assign to 'text' because it is a read-only property
    this.text += '!';
  }
}

const quote = new Quote('There is a brilliant child locked inside every student');
// Error: Cannot assign to 'text' because it is a read-only property
quote.text = 'hi';
  • 원시 타입의 초기값을 갖는 readonly로 선언된 속성은 값의 타입이 가능한 한 좁혀진 리터럴 타입으로 유추됩니다.
class RandomQuote {
  readonly explicit: string = 'Home is the nicest word there is.';
  readonly implicit = 'Home is the nicest word there is.';
  
  constructor() {
    if (Math.random() > 0.5) {
      this.explicit = "We start learning the minute we're born.";
      // Error: type "We start learning the minute we're born." 
      // is not assinable to type 'Home is the nicest word there is.'
      this.implicit = "We start learning the minute we're born.";
    }
  }
}

const quote = new RandomQoute();

quote.explicit; // 타입 : string
quote.implicit; // 타입 : 'Home is the nicest word there is.'



8.3 Classes as Types

  • 타입 시스템에서의 클래스는 클래스 선언이 런타임 값(클래스 자체)과 type annotation에서 사용할 수 있는 타입을 모두 생성한다는 점에서 상대적으로 독특합니다.
class Teacher {
  sayHello() {
    console.log('Take chances, make mistakes, get messy!');
  }
}

let teacher: Teacher;
// Ok
teacher = new Teacher();
// Error: Type 'string' is not assignable to type 'Teacher'
teacher = 'Wahoo!';
  • TypeScript는 클래스의 동일한 멤버를 모두 포함하는 모든 객체 타입을 클래스에 할당할 수 있는 것으로 간주합니다.
class SchoolBus {
  getAbilities() {
    return ['magic', 'shapeshifting'];
  }
}

function withSchoolBus(bus: SchoolBus) {
  console.log(bus.getAbilities());
}

withSchoolBus({
  getAbilities: () => ['transmogrification'],
});

withSchoolBus({
  // Error: Type 'number' is not assignable to type 'string[]'
  getAbilities: () => 123,
})



8.4 Classes and Interfaces

  • TypeScript는 클래스 이름 뒤에 implements 키워드와 interface 이름을 추가함으로써 클래스의 해당 인스턴스가 interface를 준수한다고 선언할 수 있습니다.
    • 이렇게 하면 클래스를 각 interface에 할당할 수 있어야 함을 interface에 나타냅니다.
    • 타입 검사기에 의해 모든 불일치에 대해서 타입 오류가 발생합니다.
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('...studing');
    }
  }
}

class Slacker implements Learner {
  // Error: Class Slacker incorrectly implements interface 'Learner'
  //	Property 'study' is missing in type 'Slacker' but required in type 'Learner'
}
  • 클래스가 이미 interface와 일치하는 경우, TypeScript의 타입 검사기는 interface의 인스턴스가 필요한 곳에 해당 인스턴스를 사용할 수 있도록 허용합니다.
  • TypeScript는 interface에서 클래스의 메서드 또는 속성 타입을 유추하지 않습니다.
    • 클래스의 멤버에 type annotation을 제공하지 않으면 암시적인 any 타입 오류가 발생합니다.
class Student implements Learner {
  // Error: Member 'name' implicitly has an 'any' type
  name;
  // Error: parameter 'hours' implicitly has an 'any' type
  study(hours);
}
  • interface를 구현하는 것은 순전히 안전성 검사를 위한 것입니다.
    • 모든 interface 멤버를 클래스 정의로 복사하지 않습니다.
    • 대신 interface를 구현하면 클래스 인스턴스가 사용되는 곳에서 나중에 타입 검사기로 신호를 보내고 클래스 정의에서 표면적인 타입 오류가 발생합니다.

8.4.1 Implementing Multiple Interfaces

  • TypeScript의 클래스는 다중 interface를 구현해 선언할 수 있습니다.
    • interface 목록은 interface 이름 사이에 쉼표(,)를 넣고, 개수 제한 없이 interface를 사용할 수 있습니다.
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(', ');
  }
}

class Empty implements Graded, Reporter {
  // Error: Class 'Empty' incorrectly implements interface 'Grade'
  //	Property 'grades' is missing in type 'Empty' but required in type 'Graded'
  // Error: Class 'Empty' incorrectly implements interface 'Reporter'
  //	Property 'report' is missing in type 'Empty' but required in type 'Reporter'
}
  • 실제로 한 번에 두 interface를 구현할 수 없도록 정의하는 interface가 있을 수 있습니다.
    • 2개의 충돌하는 interface를 구현하는 클래스를 선언하려고 하면, 클래스에 하나 이상의 타입 오류가 발생합니다.
interface AgeIsNumber {
  age: number;
}

interface AgeIsNotNumber {
  age: () => string;
}

class AsNumber implements AgeIsNumber, AgeIsNotNumebr {
  age = 0;
  // Error: Property 'age' in type 'AsNumber' is not assignable to the same property in base type 'AgeIsNotNumber'
 //	Type 'number' is not assignable to type '() => string'
}
  • 두 interface가 매우 다른 객체 형태를 표현하는 경우에는 동일한 클래스로 구현하지 않아야 합니다.



8.5 Extending a Class

  • TypeScript는 다른 클래스를 확장하거나 하위 클래스를 만드는 JavaScript 개념에 타입 검사를 추가합니다.
    • 기본 클래스에 선언된 모든 메서드나 속성은 파생 클래스라고도 하는 하위 클래스에서 사용할 수 있습니다.
class Teacher {
  teach() {
    console.log('The surest test of discipline is its absence');
  }
}

class StudentTeacher extends Teacher {
  learn() {
    console.log('I cannot afford the luxury of a closed mind');
  }
}

const teacher = new StudentTeacher();
// Ok, 기본 클래스에서 정의됨
teacher.teach();
// Ok, 하위 클래스에서 정의됨
teacher.learn();
// Error: Property 'other' does not exist on type 'StudentTeacher'
teacher.other();

8.5.1 Extension Assignability

  • 하위 클래스도 기본 클래스의 멤버를 상속합니다.
    • 하위 클래스의 인스턴스는 기본 클래스의 모든 멤버를 가지므로 기본 클래스의 인스턴스가 필요한 모든 곳에서 사용할 수 있습니다.
    • 기본 클래스가 하위 클래스가 가지고 있는 모든 멤버가 없으면 더 구체적인 하위 클래스가 필요할 때 사용할 수 없습니다.
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'); // Ok
lesson = new OnlineLesson('coding', 'oreilly.com'); // Ok

let online: OnlineLession;
online = new OnlineLesson('coding', 'oreilly.com'); // Ok
online = new Lesson('coding'); // Error: Property 'url' is missing in type 'Lesson'
//	but required in 'OnlineLession'
  • TypeScript의 구조적 타입에 따라 하위 클래스의 모든 멤버가 동일한 타입의 기본 클래스에 이미 존재하는 경우, 기본 클래스의 인스턴스를 하위 클래스 대신 사용할 수 있습니다.
class PastGrades {
  grades: number[] = [];
}

class LabeledPastGrades extends PastGrades {
  label?: string;
}

let subClass: LabeledPastGrades;

subClass = new LabeledPastGrades(); // Ok
subClass = new PastGrades(); // Ok

8.5.2 Overridden Constructors

  • TypeScript에서 하위 클래스는 자체 생성자를 정의할 필요 없습니다.
    • 자체 생성자가 없는 하위 클래스는 암묵적으로 기본 클래스의 생성자를 사용합니다.
  • JavaScript에서 하위 클래스가 자체 생성자를 선언하면 super 키워드를 통해 기본 클래스 생성자를 호출해야 합니다.
    • 하위 클래스 생성자는 기본 클래스에서의 필요 여부와 상관없이 모든 매개변수를 선언할 수 있습니다.
  • TypeScript 검사기는 기본 클래스 생성자를 호출할 때 올바른 매개변수를 사용하는지 확인합니다.
class GradeAnnouncer {
  message: string;
  
  constructor(grade: number) {
    this.message = grade <= 65 ? 'Maybe next tiem...' : 'You pass';
  }
}

class PassingAnnouncer extends GradeAnnouncer {
  constructor() {
    supper(100):
  }
}

class FailingAnnouncer extends GradeAnnouncer {
  constructor() {
    // Error: Constructors for subclass must contain a 'super' call
  }
}
  • TypeScript는 super()를 호출하기 전에 this 또는 super에 접근하려는 경우 타입 오류를 보고합니다.
class GradesTally {
  grades: number[] = [];
  
  addGrades(...grades: number[]) {
    this.grades.push(...grades);
    return this.grades.length;
  }
}

class ContinuedGradesTally extends GradesTally {
  constrctor(previousGrades: number[]) {
    // Error: 'super' must be called before accessing
    // 'this' in the constructor of a subclass
    this.grades = [...previousGrades];
    
    super();
    // Ok
    console.log('starting with length', this.grades.length);
  }
}

8.5.3 Overridden Methods

  • 하위 클래스의 메서드가 기본 클래스의 메서드에 할당될 수 있는 한, 하위 클래스는 기본 클래스와 동일한 이름으로 새 메서드를 다시 선언할 수 있습니다.
    • 기본 클래스를 사용하는 모든 곳에 하위 클래스를 사용할 수 있으므로, 새 메서드의 타입도 기본 메서드 대신 사용할 수 있어야 합니다.
class GradeCounter {
  counterGrades(grades: string[], letter: string) {
    return grades.filter(grade => grade === letter).length;
  }
}

class FailureCounter extends GradeCounter {
  countGrades(grades: string[]) {
  return super.counterGrades(grades, 'F');
  }
}

class AnyFailureChecker extends GradeCounter {
  counterGrades(grades: string[]) {
    // Error: Property 'counterGrades' in type 'AnyFailureChecker' 
    // is not assignable to the same property in base type 'GradeCounter'
    //	Type '(grades: string[]) => boolean' is not assignable to '(grades: string[], letter: string) => number'
    //		Type 'boolean' is not assignable to type 'number'
    return super.counterGrades(grades, 'F') !== 0;
  }
}

const counter : GradeCounter = new AnyFailureChecker();
// 예상 타입 : number
// 실제 타입 : boolean
const count = counter.counterGrades(['A', 'C', 'F']);

8.5.4 Overridden Properties

  • 하위 클래스는 새 타입을 기본 클래스의 타입에 할당할 수 있는 한, 동일한 이름으로 기본 클래스의 속성을 명시적으로 다시 선언할 수 있습니다.
    • 하위 클래스는 기본 클래스와 구조적으로 일치해야 합니다.
    • 속성을 다시 선언하는 대부분의 하위 클래스는 해당 속성을 union 타입의 더 구체적인 하위 집합으로 만들거나 기본 클래스 속성 타입에서 확장되는 타입으로 만듭니다.
class Assignment {
  grade?: number;
}

class GradedAssignment extends Assignment {
  grade: number;
  
  constructor(grade: number) {
    super();
    this.grade = grade;
  }
}
  • 속성의 union 타입의 허용된 집합을 확장할 수는 없습니다.
    • 만약 확장한다면 하위 클래스 속성은 더 이상 기본 클래스 속성 타입에 할당할 수 없습니다.
class NumericGrade {
  value = 0;
}

class VagueGrade extends NumericGrade {
  value = Math.random() > 0.5 ? 1 : '...';
  // Error: Property 'value' in type 'NumberOrString' is not 
  // assignable to the same property in base type 'JustNumber'
  //	Type 'string | number' is not assignable to type 'number'
  //		Type 'string' is not assignable to 'number'
}

const instance : NumericGrade = new VagueGrade();
// 예상 타입 : number
// 실제 타입 : number | string
instance.value;



8.6 Abstract Classes

  • 때로는 일부 메서드의 구현을 선언하지 않고, 대신 하위 클래스가 해당 메서드를 제공할 것을 예상하고 기본 클래스를 만드는 방법이 유용할 수 있습니다.
  • 추상화하려는 클래스 이름과 메서드 앞에 타입스크립트 abstact 키워드를 추가합니다.
    • 이러한 추상화 메서드 선언은 추상화 기본 클래스에서 메서드의 본문을 제공하는 것을 건너뛰고, 대신 interface와 동일한 방식으로 선언됩니다.
abstact class School {
  readonly name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  abstact getStudentTypes(): string[];
}

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

class Absence extends School {
  // Error: Nonabstact class 'Absence' does not implement
  // inherited abstact member 'getStudentTypes' from class 'School'
}

let school: School;
// Ok
school = new Preschool('Sunnyside Daycare');
// Error: Cannot create an instance of an abstact class
school = new Shool('somewhere else');
  • 추상 클래스는 클래스의 세부 사항이 채워질 거라 예상되는 프레임워크에서 자주 상용됩니다.
  • 새 인스턴스를 생성하려면 추상 클래스가 아니라 추상 클래스에서 파생된 하위 클래스를 사용해야 합니다.



8.7 Member Visibility

  • JavaScript는 클래스 멤버 이름 앞에 #을 추가해 private 클래스 멤버임을 나타냅니다.

    • private 클래스 멤버는 해당 클래스 인스턴스에서만 접근할 수 있습니다.
    • JavaScript 런타임은 클래스 외부 코드 영역에서 private 메서드나 속성에 접근하려고 하면 오류를 발생시킵니다.
  • TypeScript는 private 클래스 멤버를 지원하지만, 타입 시스템에만 존재하는 클래스 메서드와 속성에 대해 조금 더 미묘한 프라이버시 정의 집합을 허용합니다.

  • TypeScript의 멤버 접근성은 클래스 멤버의 선언 이름 앞에 다음 키워드 중 하나를 추가해 만듭니다.

    • public(기본값) : 모든 곳에서 누구나 접근 가능
    • protected : 클래스 내부 또는 하위 클래스에서만 접근 가능
    • private : 클래스 내부에서만 접근 가능
  • 이러한 접근성 키워드는 순수하게 타입 시스템 내에 존재합니다.

class Base {
  isPublicImplicit = 0;
  public isPublicExplicit = 1;
  protected isProtected = 2;
  private isPrivate = 3;
  #truePrivate = 4;
}

class SubClass extends Base {
  example() {
    // Ok
    this.isPublicImplicit;
    // Ok
    this.isPublicExplicit;
    // Ok
    this.isProtected;
    
    // Error: Property 'isPrivate' is private and only accessible within class 'Base' 
    this.isPrivate;
    // Error: Property '#truePrivate' is not accessible outside class 'Base'
    // because it has a private identifier
    this.#truePrivate;
  }
}

// Ok
new Subclass().isPublicImplicit;
// Ok
new Subclass().isPublicExplicit;
// Error: Property 'isProtected' is protected
// and only accessible witin class 'Base' and its subclasses.
new Subclass().isProtected;
// Error: Property 'isPrivate' is private
// and only accessible witin class 'Base'.
new Subclass().isPrivate;
  • TypeScript의 멤버 접근성은 타입 시스템에서만 존재하는 반면, JavaScript의 private 선언은 런타임에도 존재한다는 점이 주요 차이점입니다.
    • protected 또는 private으로 선언된 TypeScript 클래스 멤버는 명시적 또는 암묵적으로 public으로 선언된 것처럼 동일한 JavaScript 코드로 컴파일 됩니다.
class TwoKeywords {
  private readonly name: string;
  
  constructor() {
    this.name = 'Anne Sullivan'; //Ok
  }
  
  log() {
    console.log(this.name); // Ok
  }
}

const two = new TwoKeywords();
// Error: Property 'name' is private and only accessible within class 'TwoKeyword'
// Cannot assign name because it is a read-only property
two.name = 'Savitribai Phule';
  • TypeScript의 이전 멤버 접근성 키워드를 JavaScript의 # private 필드와 함께 사용할 수 없습니다.
    • # private 필드는 기본적으로 항상 private이므로 private 키워드를 추가로 표시할 필요가 없습니다.

8.7.1 Static Field Modifiers

  • JavaScript는 static 키워드를 사용해 클래스 자체에서 멤버를 선언합니다.
  • TypeScript는 static 키워드를 단독으로 사용하거나 readonly와 접근성 키워드를 함께 사용할 수 있도록 지원합니다.
    • 함께 사용할 경우, 접근성 키워드, static, readonly 키워드 순으로 작성합니다.
class Question {
  protected static readonly answer: 'bash';
  protected static readonly propmt = 
  "What's an orge's favorite programming language?";
  
  guess(getAnswer: (prompt: string) => string) {
    const answer = getAnswer(Question.propmt);
    // Ok
    if (answer === Question.answer) {
      console.log('you got it');
    } else {
      console.log('Try again');
    }
  }
}

// Error: Property 'answer' is protected and only
// accessible within class 'Question' and subclasses.
Question.answer;
  • static 클래스 필드에 대해 readonly와 접근성 제한자를 사용하면, 해당 필드가 해당 클래스 외부에서 접근되거나 수정되는 것을 제한하는 데 유용합니다.

0개의 댓글