TypeScript. 타입종류(Class)

cm·2024년 1월 24일

타입스크립트

목록 보기
9/9

타입종류(Class)

  • 클래스 구조와 멤버에 대한 타입을 정의하는 방법
  • 런타임값 또는 타입 애너테이션은 모두 클래스 선언을 통해 이루어진다.

1. 클래스타입 정의

1-1. 매개변수 정의

  • 필요한 인수와 타입을 지정한다.(미지정시 any타입으로 간주된다.)
class Greeter {
    constructor(name: string) {
        /* name만 쓸경우 any로 유추*/
        console.log(`${name}, hello~~`);
    }
}
new Greeter("Gim");
/* new Greeter() // Error : 인수 개수*/

1-2. 속성 정의

  • 타입스크립트는 생성자 내에서 값 할당시 멤버의 클래스내 존재여부를 추론하지 않기 때문에 명시적으로 클래스에 속성을 선언해야 한다.
    • 필수 속성 : 반드시 기입해야 하는 속성
    • 선택 속성 : 생략 가능한 속성(생략시 디폴트값: undefined)
    • 읽기전용 속성(readonly) : 속성을 읽기 전용으로 변환. 생성자 또는 선언된 위치에서만 초기값이 할당가능하며 수정이 불가하다. 만약, 원시값으로 초깂값이 설정되면 리터럴 타입으로 추론된다.
class Trip {
    destination: string; /* 명시적으로 속성 선언 */
    constructor(destination: string) {
        this.destination = destination;
        console.log(`our trip : ${destination}`);
        /* this.year = year; /* Error : 명시적으로 속성 선언 안함*/
    }
}

readonly a : string = "abc" /* string 타입*/
readonly b = "abc" /* 리터럴 타입 (abc)*/

1-3. 함수 정의

  • 2가지 방법
    • 값이 함수인 속성을 선언하는 방법 : 명시적으로 클래스에 속성을 선언한 뒤, 생성자 안에서 값을 할당한다.
    • 메서드 접근 방식 : 클래스 안에서 메서드로 정의한다.
      • 메서드 체인으로 연결되는 프로토 타입에 정의된 메서드
      • 각 인스턴스 객체마다 새롭게 할당되는 메서드
class Person {
    name: string;
    get3: () => void; /* 함수 속성으로 정의 */
    constructor(name: string) {
        this.name = name;
        this.get3 = () => {
            console.log(`my name is ${this.name}. from get3()`);
        };
    }
    /* 메서드로 정의 (메서드 체인으로 연결되는 프로토 타입에 정의된 메서드) */
    get1() {
        console.log(`my name is ${this.name}. from get1()`);
    }
    /* 메서드로 정의 (각 인스턴스 객체마다 새롭게 할당되는 메서드) */
    get2 = () => {
        console.log(`my name is ${this.name}. from get2()`);
    };
}

2. 초기화 검사

  • undefined 타입으로 선언된 클래스의 각 속성이 생성자 내부에서 값할당이 되었는지 확인하는 검사이다. 만약, undefined 타입의 속성에 값할당이 안된 경우, 'undefined'로 판단되나 undefined 타입이 아닌 속성에 값할당이 안된경우 에러가 발생한다.
  • ! : 속성이 처음 시작되기 전에 undefined값이 할당된다. 의도적으로 클래스 속성에 값을 할당안하고 싶을 때 표기하면 검사를 비활성화시킨다.

class withTS {
    a = 0;
    b: number;
    c: number | undefined;
    d: number; /* Error : 초기화검사시 생성자에서 값할당 안됨 */
    constructor() {
        this.b = 1;
    }
}

class withTS {
    a = 0;
    b: number;
    c: number | undefined;
    d!: number; /*Error : 초기화검사시 생성자에서 undefined 할당*/
    constructor() {
        this.b = 1;
    }
}
const aaa = new withTS();
console.log(aaa.a); //0
console.log(aaa.b); //1
console.log(aaa.c); //undefined
console.log(aaa.d); //undefined

3. 값할당

  • 타입스크립트에서의 클래스는 클래스 생성클래스 타입 표기 두가지 역할을 한다.
class Teacher {
    sayhello() {
        return ["hi", "hello"];
    }
}
function withT(teacher: Teacher) {
    console.log(teacher.sayhello());
}
let teacher: Teacher = new Teacher(); /* 클래스 값 할당*/
withT(new Teacher()); /* ["hi", "hello"]*/
withT({ sayhello: () => ["hah"] }); /* 동일한 멤버를 모두 포함하는 객체 타입 할당 ["hah"]*/
/* withT({ sayhello: () => 123 }); // Error : 객체의 반환 타입이 다름*/
  • withT(new Teacher()): Teacher 타입의 teacher 인스턴스가 withT의 인자로 들어가 함수 호출된다.
  • withT( { sayhello : () => ['hah'] }): Teacher 타입과 동일한 객체인 { sayhello : () => ['hah'] }가 클래스 인스턴스처럼 할당가능하다.

4. 인터페이스 이용한 클래스 구현

  • 인터페이스 앞에 키워드implements를 표기하여 클래스를 구현한다.
  • 인터페이스를 구현한 클래스에는 인터페이스의 선택적 속성을 제외한 모든 속성이 정의되어 클래스에 있어야 한다.
  • 인터페이스를 구현한 클래스는 인터페이스에서 정의한 타입보다 구체적인 타입으로 구현해야 한다.
  • 인터페이스를 구현한 클래스에는 인터페이스에 없는 속성을 추가할 수 있다.
  • 인터페이스를 구현한 클래스에서는 인터페이스에서 정의한 타입을 내로잉 해서 사용할 수 있지만 any 타입으로도 사용할 수 있다.
    • Why? any 타입은 컴파일러가 타입 검사를 수행하지 않는 타입으로 해당 값에 대한 타입 검사를 수행하지 않기 때문이다.(위험하므로 비권장)
interface Learner {
    name: string | number;
    study(hours: number): void;
}
/* 인터페이스의 모든 속성이 정의되어 클래스에 있어야 한다.(선택적 속성 제외) */
class Student implements Learner {
    name: string; /* string | number 내에서 구현가능하다. (any도 가능) */
    age : number; /* 인터페이스에 없는 속성 추가 가능하다. */
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age
    }
    study(hours: number) {
        for (let i = 0; i < hours; i += 1) {
            console.log("...study");
        }
    }
}
  • 다중 인터페이스 이용가능.
    • 같은 속성의 다른 타입을 가진 인터페이스는 사용이 불가하다.
    • 사용하는 각각의 인터페이스에서 동일한 속성/메서드명을 사용하는 것을 비권장한다.(차라리, 다른 클래스로 구현하거나 공통 타입을 추가할 것)
/* 다중 상속예시 */
interface Daughter{
    favorite : string
}
interface Learner {
    name: string | number;
    study(hours: number): void;
}
/* ,를 통해 다중상속 가능하다. */
class Student implements Learner, Daughter {
    name: string; /* string | number 내에서 구현가능하다. (any도 가능) */
    favorite : string; /* string 내에서 구현가능하다. (any도 가능) */
    age : number; /* 인터페이스에 없는 속성 추가 가능하다. */
    constructor(name: string, age: number, favorite: string) {
        this.name = name;
        this.age = age;
        this.favorite = favorite;
    }
    study(hours: number) {
        for (let i = 0; i < hours; i += 1) {
            console.log("...study");
        }
    }
}


/* 같은 속성의 다른 타입을 가진 인터페이스 사용 불가*/
interface Daughter{
    // name : null /* 같은 속성의 다른 타입을 가진 인터페이스는 같이 쓸 수 없다. */
    name : string /* 가능 */
}
interface Learner {
    name: string | number;
    study(hours: number): void;
}
/* ,를 통해 다중상속 가능하다. */
class Student implements Learner, Daughter {
    name: string; /* string | number 내에서 구현가능하다. (any도 가능) */
    age : number; /* 인터페이스에 없는 속성 추가 가능하다. */
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    study(hours: number) {
        for (let i = 0; i < hours; i += 1) {
            console.log("...study");
        }
    }
}

5. 클래스 확장 : extends

5-1. 확장방법

  • 키워드 extends로 확장한다.
class Teacher {
    subject: string;
    constructor(subject: string) {
        this.subject = subject;
    }
}
class StudentTeacher extends Teacher {
    url: string;
    constructor(subject: string, url: string) {
        super(subject);
        this.url = url;
    }
}

let student1: Teacher;
let student2: Teacher;
student1 = new Teacher("coding");
student2 = new StudentTeacher("coding", "https://");

let student3: StudentTeacher;
let student4: StudentTeacher;
/* student3 = new Teacher("coding");  Error : Property 'url' is missing in type 'Teacher' but required in type 'StudentTeacher'. */
student4 = new StudentTeacher("coding", "https://");


/* 선택속성으로 만들어 에러 방지. 좋은 코드처럼 보이지는 않지만 가능은 하다. */
class StudentTeacher extends Teacher {
    url?: string;
    constructor(subject: string, url?: string) {
        super(subject);
        this.url = url;
    }
}
let student3: StudentTeacher;
let student4: StudentTeacher;
student3 = new Teacher("coding");
student4 = new StudentTeacher("coding", "https://");
  • 하위클래스를 타입으로 가지는 변수가 상위클래스를 생성하여 값 할당하면 포함되어 있지 않은 속성이 존재할 수 있기 때문에 에러가 발생할수도 있다.(선택 속성화하면 에러를 막을 수는 있다.)

5-2. 클래스 상속시 생성자/속성/메서드 재정의

  • 생성자 : 자동으로 부모의 생성자를 상속받으므로 추가 속성이 필요하지 않을 경우 생성자를 정의할 필요없다. 그러나, 추가 속성이 필요하면 키워드 super 로 부모의 생성자를 호출하여 받는다.(단, this와 super 를 호출하기 전에 super()를 제일 위에서 선언해야 한다.) 그 후, 추가 속성을 표기한다.(생성자 내에서 값 할당시 멤버의 클래스내 존재여부를 확인하지 않으므로 생성자 밖에 명시적으로 속성 표기도 해줘야 한다.)
  • 속성 : 부모클래스의 메서드를 재정의하고 싶을 때는 자식클래스는 재정의할 속성을 부모클래스의 속성타입보다 더 구체적으로 정의해야 한다.
  • 메서드 : 부모클래스의 메서드를 재정의하고 싶을 때는첫번째 매개변수반환타입만 동일하면 재정의할 수 있다.
    class GrandCounter {
      count(grades: string[], letter: string) {
          return grades.length;
      }
    }
    /* 메서드에서는 첫번째 매개변수와 반환타입만 동일하면 허용한다. */
    class Counter1 extends GrandCounter {
      count(grades: string[]) {
          return grades.length;
      }
    }
    /*첫번째 매개변수는 동일하나 반환타입이 다르므로 에러발생한다. */
    class Counter2 extends GrandCounter {
      count(grades: string[]) {
          return true; //Error
      }
    }

6. 멤버 접근성

6-1. 접근제한자

  • 접근제한자를 이용하여 접근성을 통제한다.
    • public(default) : 모든 곳에서 누구나 접근 가능
    • protected : 클래스 내부 또는 하위클래스에서만 접근 가능
    • private : 클래스 내부에서만 접근가능(#)
    /* readonly는 접근 제한자 뒤에 표시한다. */ 
    class One {
        private readonly name: string;
        constructor() {
            this.name = "kim";
        }
    }
    const one = new One();

6-2. 정적필드 제한자

  • 정적 필드 : 클래스의 인스턴스가 아닌 클래스 자체에 속하는 필드.

  • 정적 필드 제한자를 사용하여 정적 필드를 사용하면 클래스 외부에서 이 필드에 대한 접근을 제한할 수 있다.

    • 정적 필드 제한자 : readonly, static

      /* 'answer'는 public이기에 외부에서 읽을 수 있고 static이기 때문에 클래스에 접근이 가능하다. 'prompt'는 protected이기에 외부에서 접근이 불가하고 static이기 때문에 클래스에 접근이 가능하다. 그러나, 이 코드에서는 클래스 내부/하위클래스에서 접근하는 과정이 없으므로 접근이 불가하다. */
      class Question {
        public static readonly answer = "bash"; /* 정적필드 제한자 */
        protected static readonly prompt = "what"; /* 정적필드 제한자 */
        guess(getAnswer: (prompt: string) => string) {
            const answer = getAnswer(Question.prompt);
            if (answer === Question.prompt) {
                console.log("You got it");
            } else {
                console.log("Try");
            }
        }
      }
      const question = new Question();
      console.log(Question.answer) /* bash */
      // console.log(Question.prompt) /* Error : 접근 불가 */
      // question.answer /* Error : 접근 불가 */
      // question.prompt /* Error : 접근 불가 */
      

7. 추상클래스

  • 추상클래스(상위클래스)에서 일부 메서드를 구현하지 않고 하위클래스에서 메서드를 정의한다.
    • 추상클래스 선언 : 추상화하려는 클래스명메서드명 앞에 키워드 abstract 추가한다.
    • 추상클래스를 구현할 하위클래스 : extends를 이용하여 추상클래스를 상속받는다.
    • 일부 메서드가 구현되어 있지 않기 때문에 인스턴스화가 불가능하다.
/* 메서드 getStudent를 구현하지 않은 추상클래스 School */
abstract class School {
    readonly name: string;
    constructor(name: string) {
        this.name = name;
    }
    abstract getStudent(): string[];
}
/* 메서드 getStudent가 구현되지 않았으므로 에러 발생 */
class PreSchool1 extends School {} 
/* 메서드 getStudent가 구현*/
class PreSchool2 extends School {
    getStudent(): string[] {
        return ["pre"];
    }
}
profile
나를 위한 기록

0개의 댓글