Classes[TypeScript]

SnowCat·2023년 2월 20일
0

Typescript - Handbook

목록 보기
8/9
post-thumbnail

Class Members

  • 자바스크립트와 동일하게 클래스를 사용가능하며, 타입스크립트에서는 클래스 멤버들의 타입을 추론해줌

Fields

class Point {
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;
pt.x = "0"; // Type 'string' is not assignable to type 'number'.
  • 옵션에서 strictPropertyInitialization을 킬 경우 클래스 멤버들은 constructor에서 초기화 되어야 함
class BadGreeter {
  name: string; //Property 'name' has no initializer and is not definitely assigned in the constructor.
}

class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

// 생성자를 사용해 초기화해야할 상황이 아니면 ! 연산자를 사용
class OKGreeter {
  name!: string; //ok
}

Readonly

  • Readonly 속성을 사용해 값이 프로퍼티 값이 수정되는 것을 막을 수 있음
class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
 
  err() {
    this.name = "not ok"; //Cannot assign to 'name' because it is a read-only property.
  }
}
const g = new Greeter();
g.name = "also not ok"; //Cannot assign to 'name' because it is a read-only property.

Constructors

  • 함수를 호출하듯이 생성자를 호출 가능
class Point {
  x: number;
  y: number;
 
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
  
  // 오버로딩
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {

}
  • 함수와는 다르게 타입 파라미터를 가질 수 없고, 반환값의 타입을 지정할 수 없음
  • 부모 클래스가 있으면 생성자 내에서 this를 사용하기 전에 super()로 호출해야 함
class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    console.log(this.k); // // 'super' must be called before accessing 'this' in the constructor of a derived class.
	// 먼저와야함
    super();
  }
}

Methods

  • 클래스 내에 있는 함수 속성은 메서드로 부름
  • 함수 내의 속성들은 this로 접근해야 함에 주의
let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    // this.x로 적지 않으면 C가 있는 scope의 x=0 참조
    x = "world";
Type 'string' is not assignable to type 'number'.
  }
}

Getters / Setters

  • 클래스 내에서 Getter, Setter를 구현 가능
    다른 추가적인 로직이 없는 Getter, Setter를 사용할 때에는 public field로 변수를 그냥 사용하는 것을 권장
class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}
  • 타입스크립트는 타입 추론규칙은 다음과 같음
    • getter만 있고 setter가 없으면 프로퍼티는 readonly로 추론
    • setter의 타입을 알 수 없으면 getter의 타입을 반환하는 것으로 추론
    • getter, setter의 Member Visibility는 같아야 함

Index Signatures

  • 클래스 내에서 index signature 사용 가능
    but 사용하기 어렵기 떄문에 클래스 외부에 index signature 값을 저장하는 것을 권장
class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
 
  check(s: string) {
    return this[s] as boolean;
  }
}

Class Heritage

  • 다른 언어들과 유사하게 자바스크립트에서도 다른 클래스들을 상속 가능함

implements

  • implement를 사용해 클래스 인터페이스를 구현 가능
  • implement를 사용할때는 여러개의 클래스를 동시에 상속 가능함
interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
Class 'Ball' incorrectly implements interface 'Pingable'.
  // Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
  pong() {
    console.log("pong!");
  }
}
  • implement를 사용하면 기존에 선언된 인터페이스의 타입이 클래스의 타입이나 메서드에 영향을 주지 못하게 된다는 점에 주의
interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) { // Parameter 's' implicitly has an 'any' type.
    return s.toLowercse() === "ok"; // any로 추론되기 떄문에 여기에서는 오류가 없음
  }
}

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
c.y = 10; // Property 'y' does not exist on type 'C'.

extends

  • extends를 사용하면 부모 클래스가 가진 모든 메서드와 프로퍼티를 가지고 있게 됨
class Animal {
  move() {
    console.log("Moving along!");
  }
}
 
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
 
const d = new Dog();
d.move();
d.woof(3);
  • super를 사용해 부모 클래스 값을 가져옴으로써 부모 클래스의 프로퍼티를 참조할 수 있음
class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {

  greet(name?: string) {
    if (name === undefined) {
      // super 사용
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}
 
const d = new Derived();
d.greet();
d.greet("reader");

Type-only Field Declarations

  • ES2022 이상 자바스크립트로 컴파일 하거나 useDefineForClassFields 옵션을 켜두면 자식 클래스는 부모 클래스가 생성된 이후에 생성되 부모 클래스의 모든 값들을 덮어쓰게됨
  • 타입을 구체적으로 선언하는 등의 상황으로 부모 타입을 수정해야 하는경우 declare 사용을 통해 해결해야 함
interface Animal {
  dateOfBirth: any;
}
 
interface Dog extends Animal {
  breed: any;
}
 
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
 
class DogHouse extends AnimalHouse {
  // js 코드에는 영향을 주지 않음
  declare resident: Dog;
  constructor(dog: Dog) {
    super(dog);
  }
}

Initialization Order

  • 클래스의 초기화 순서는 base 클래스의 field -> 생성자 => derived class의 field -> 생성자 순으로 진행됨
class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {
  name = "derived";
}
 
const d = new Derived(); // 원래 클래스의 생성자가 생성될 때 "My name is base" 출력

Member Visibility

public

  • 특별히 값을 설정하지 않으면 프로퍼티는 public 값을 가짐
class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

protected

  • protected 프로퍼티는 선언된 클래스와 클래스를 상속받은 자식 클래서만 확인할 수 있음
class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
 
class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
g.getName();
  • 메서드 상속시 오버라이딩 한 프로퍼티를 public으로 설정하면 그 값은 public으로 취급됨
class Base {
  protected m = 10;
}
class Derived extends Base {
  m = 15; // 따로 public 기호를 적지 않더라도 public이 됨에 주의
}
const d = new Derived();
console.log(d.m); //가능
  • protected 프로퍼티는 다중 상속이 불가능함
class Base {
  protected x: number = 1;
}
class Derived1 extends Base {
  protected x: number = 5;
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }
  f2(other: Base) {
    /*Property 'x' is protected and only accessible through an instance of
    class 'Derived2'. This is an instance of class 'Base'. */
    other.x = 10;
  }
}

private

  • 선언한 클래스 자신만 볼 수 있는 속성은 private 사용
class Base {
  private x = 0;
}
const b = new Base();
console.log(b.x); // Property 'x' is private and only accessible within class 'Base'.

class Derived extends Base {
  showX() {
    console.log(this.x); // Property 'x' is private and only accessible within class 'Base'.
  }
}
  • 같은 클래스끼리의 private 속성은 접근 가능
class A {
  private x = 10;
 
  public sameAs(other: A) {
    return other.x === this.x; //ok
  }
}
  • private, protected 속성은 컴파일 중에 사라지고, 컴파일 이전에도 대문자 표기법으로 접근이 가능함에 주의
  • 이를 원하지 않으면 클로저, weakmap, 자바스크립트 자체 private field(#)을 사용해서 접근을 막아야 함
class MySafe {
  private secretKey = 12345;
}
const s = new MySafe();

//Property 'secretKey' is private and only accessible within class 'MySafe'.
console.log(s.secretKey); 
// ok
console.log(s["secretKey"]);

Static Members

  • static을 표기한 프로퍼티들은 생성자 객체 자체만으로 접근이 가능함
  • static을 사용해도 public, protected, private 속성 사용 가능
class MyClass {
  static x = 0;
  private static y = 1;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();
console.log(MyClass.y); // Property 'y' is private and only accessible within class 'MyClass'.
  • static 속성은 다른 속성처럼 상속됨
class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting();
}

Special Static Names

  • 클래스 자체가 new로 호출되는 함수이기 때문에 static 메서드를 사용할 때는 function 프로토 타입의 예약어 사용 불가능
// name, length, call 사용 불가능
class S {
  // Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
  static name = "S!";
}

Why No Static Classes?

  • 자바나 C#과 같은 언어는 모든 데이타와 함수가 클래스 내부에 있어야 하기 때문에 static 클래스를 사용하는 경우가 있음
  • 반면 자바스크립트는 그렇지 않기 때문에 static classes를 사용할 이유가 없음
// 이렇게 사용하지 말 것
class MyStaticClass {
  static doSomething() {}
}
 
// 함수를 직접 선언하거나
function doSomething() {}
 
// 평범한 객체로 선언할 것
const MyHelperObject = {
  dosomething() {},
};

Static Blocks in Classes

  • 클래스 내부에 static field는 초기화 중에 클래스의 private에 접근하면서 명령문을 작성할 수 있음
  • 이를 통해 변수의 노출 없이 클래스 초기화를 하거나, 예외처리를 진행할 수 있음
class Foo {
    static #count = 0;
 
    get count() {
        return Foo.#count;
    }
 
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

Generic Classes

  • 클래스도 제네릭을 가질 수 있음
class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
 
const b = new Box("hello!"); // Box<String>

// static 프로퍼티에는 사용이 불가능함
class Box<Type> {
  static defaultValue: Type; //Static members cannot reference class type parameters.
}

This at Runtime in Classes

  • 함수 내부에서의 this는 함수의 상위 스코프 참고
class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName, // == func () {return this.name;}
};
const letError = c.getName
 
// this = obj
console.log(obj.getName()); //obj
// this = window => this.name = undefined
console.log(letError()); //Cannot read properties of undefined (reading 'name') 
  • 화살표 함수를 쓰면 상위 컨텍스트의 this를 보장받으나, 다음을 유의해야 함
    • 각 클래스 인스턴스가 함수의 정의를 복사 -> 메모리 사용 증가
    • 프로토타입 체인에 등록되지 않기 때문에 super.getName으로 값을 가져올 수 없음
class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name; // This: Myclss로 고정
  };
}
const c = new MyClass();
const g = c.getName;
console.log(g()); // MyClass

This parameters

  • 타입스크립트에서는 직접 this로 바인딩 가능한 대상을 지정할 수 있음
  • 타입스크립트는 this가 사용되는 context를 분석해 this에 바인딩 된 값 이외 다른 값이 들어오면 오류를 출력함
  • 화살표 함수와는 반대의 특성을 가짐
    • 타입스크립트가 검사하지 못한 값은 여전히 잘못 this가 사용될 위험이 있음
    • 인스턴스가 아닌 클래스당 하나의 함수 정의를 가짐
    • 여전히 메서드는 super로 호출 가능함
class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
c.getName(); //MyClass
 
// this !== MyClass => 에러 발생
const g = c.getName;
console.log(g()); // The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.

This Types

  • This에 값이 동적으로 들어가는 위치에서 타입스크립트는 타입을 this로 추론함
class Box {
  contents: string = "";
  set(value: string) { // (value: string) => this
    this.contents = value;
    return this;
  }
}
  • this 타입을 타입 단언에서도 사용 가능
class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

class DerivedBox extends Box {
  otherContent: string = "?";
}

// 같은 클래스로 생성된 인스턴스끼리만 this 타입 할당 가능
const base = new Box();
const derived = new DerivedBox();
/*
Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
  Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
*/
derived.sameAs(base);

This based type guards

  • this를 타입 가드에 사용해 타입 범위를 좁히는데 사용 가능
class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}
 
class Directory extends FileSystemObject {
  children: FileSystemObject[];
}
 
interface Networked {
  host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
  fso.content; //FileRep
} else if (fso.isDirectory()) {
  fso.children; //Directory
const fso: Directory
} else if (fso.isNetworked()) {
  fso.host; //Networked & FileSystemObject
}

Class Expressions

  • 익명 함수와 비슷한 방법으로 클래스 표현식 사용 가능
const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
 
const m = new someClass("Hello, world"); //someClass<string>

Abstract Classes and Members

  • 클래스, 메서드, 필드는 abstract를 가질 수 있음
  • abstract 클래스 내에 abstract 메서드, 필드를 가질 수 있음
  • 메서드나 필드가 abstract인 경우 이를 사용하는 subclass들이 abstract 부분을 직접 구현해야 함
abstract class Base {
  abstract getName(): string;
 
  printName() {
    console.log("Hello, " + this.getName());
  }
}
 
//concrete : 클래스 내부에 추상 멤버를 가지지 않는 경우
const b = new Base(); // Cannot create an instance of an abstract class.

// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
class Derived extends Base {
}

class Derived extends Base {
  getName() {
    return "world";
  }
}
 
const d = new Derived();
d.printName();

Abstract Construct Signatures

  • 클래스 내부에서 다른 인스턴스를 가져오기 위해서는 Construct Signatures를 가져와야 함
function greet(ctor: typeof Base) {
  const instance = new ctor(); // Cannot create an instance of an abstract class.
  instance.printName();
}
  • 추상 클래스 자체는 클래스 내부에서 생성자로 불러올 수 없음
function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
/*
Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'.
  Cannot assign an abstract constructor type to a non-abstract constructor type.
*/
greet(Base);

Relationships Between Classes

  • 클래스의 내용물이 같으면 이름이 달라도 같은 클래스로 취급함
class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
const p: Point1 = new Point2(); //ok
  • 한 클래스가 다른 클래스의 부분집합이면 명시적인 상속 표기 없이 범위가 넓은 클래스를 사용 가능함
class Person {
  name: string;
  age: number;
}
 
class Employee {
  name: string;
  age: number;
  salary: number;
}
 
const p: Person = new Employee(); //ok

class Empty {}
 
function fn(x: Empty) {
}
 
// 전부 가능
fn(window);
fn({});
fn(fn);

출처:
https://www.typescriptlang.org/docs/handbook/2/classes.html#extends-clauses

profile
냐아아아아아아아아앙

0개의 댓글