[TypeScript 독학] #4 연산자, 클래스, 제네릭

안광의·2022년 2월 21일
0

TypeScript 독학

목록 보기
4/12
post-thumbnail

시작하며

이번에는 연산자를 이용한 타입 정의와 클래스, 제네릭에 대해서 정리하려고 한다. 자바스크립트에서 조건문에서 자주 사용하는 ||(and 연산자) &&(or 연산자)처럼 타입 지정도 같은 방식으로 할 수 있는 연산자와 타입스크립트에서의 클래스 사용법, C#, Java 등의 언어에서도 사용되는 제네릭에 대해서 공부하면서 타입 지정 방법을 정리하고 코드 예시를 보면서 사용법을 익힐 예정이다.

연산자를 이용한 타입 정의

Union Type

function unionText(text: string | number) {
  // ...
}

유니온 타입(Union Type)이란 자바스크립트의 OR 연산자(||)와 같이 A이거나 B이다라는 의미의 타입으로 두가지 이상의 타입이 올 경우에 | 연산자를 사용하여 여러개의 타입을 연결하는 방식이다.

any 타입을 사용하는 것보다 타입을 제한할 수 있기 때문에 타입스크립트의 이점을 살리면서 여러 타입을 정의할 수 있다.

Intersection Type

interface Person {
  name: number;
  age: number;
}
interface Developer {
  name: string;
  skill: number;
}
type Capt = Person & Developer;

인터섹션 타입(Intersection Type)은 여러 타입을 모두 만족하는 하나의 타입을 의미한다. 위 코드는 Person 인터페이스의 타입 정의와 Developer 인터페이스의 타입 정의를 & 연산자를 이용하여 합친 후 Capt 이라는 타입에 할당한 코드로 결과적으로 Capt의 타입은 아래와 같이 정의된다.

{
  name: string;
  age: number;
  skill: string;
}

Union Type 사용 시 주의할 점

interface Person {
  name: string;
  age: number;
}
interface Developer {
  name: string;
  skill: string;
}
function introduce(someone: Person | Developer) {
  someone.name; // O 정상 동작
  someone.age; // X 타입 오류, Property 'age' does not exist on type 'Person | Developer'. Property 'age' does not exist on type 'Developer'.
  someone.skill; // X 타입 오류, Property 'skill' does not exist on type 'Person | Developer'. Property 'skill' does not exist on type 'Person'.
}

여기서 introduce() 함수의 파라미터 타입을 Person, Developer 인터페이스의 유니온 타입으로 정의하였기 때문에 Person도 되고 Developer도 될테니까 함수 안에서 당연히 이 인터페이스들이 제공하는 속성들인 age나 skill를 사용할 수 있다고 생각할 수 있다. 하지만, 타입스크립트 관점에서는 introduce() 함수를 호출하는 시점에 Person 타입이 올지 Developer 타입이 올지 알 수가 없기 때문에 어느 타입이 들어오든 간에 오류가 안 나는 방향으로 타입을 추론하여야 한다.

introduce() 함수 안에서는 별도의 타입 가드(Type Guard)를 이용하여 타입의 범위를 좁히지 않는 이상 기본적으로는 PersonDeveloper 두 타입에 공통적으로 들어있는 속성인 name만 접근할 수 있게 된다.

타입가드란?

function sample(data: number | string) : void { 
  if (typeof data === 'string') { 
    console.log(data); 
  } else {
    console.log(data + 1); 
  }
}

타입 가드란 타입을 예측할 수 있도록 코드를 작성해서 버그가 발생하지 않도록 예방하는 방법을 말한다.



클래스

get & set

class Developer {
  private _name: string = ''  
  get name(): string {
    return this._name;
  }
  set name(newValue: string) {
    if (newValue && newValue.length > 5) {
      throw new Error('이름이 너무 깁니다');
    }
    this._name = newValue;
  }
}
const josh = new Developer();
josh.name = 'JOSH CARROT' //error, 이름이 너무 깁니다.

게터와 세터는 클래스의 프로퍼티 입력 값에 제한을 두어 올바른 값을 입력할 수 있게 하는 자바스크립트에 있는 기능인데, 실제로 사용한 적이 없어서 타입스크립트를 하면서 다시 복습한다는 생각으로 정리하였다. name이라는 프로퍼티에 할당할 때 set name 메소드가 실행되고 해당 값을 불러올때 get name 메소드가 실행되어 조건을 설정할 수 있다. 유의할 점은 get, set 메소드 내에서 동일한 이름의 name을 사용하면 무한으로 get, set 메소드가 실행되기 때문에 _name처럼 다른 이름으로 설정해야 한다.

get만 선언하고 set을 선언하지 않는 경우에는 자동으로 readonly로 인식된다.

Abstract Class

abstract class Developer {
  abstract coding(): void; // 'abstract'가 붙으면 상속 받은 클래스에서 무조건 구현해야 함
  drink(): void {
    console.log('drink sth');
  }
}
class FrontEndDeveloper extends Developer {
  coding(): void {
    // Developer 클래스를 상속 받은 클래스에서 무조건 정의해야 하는 메서드
    console.log('develop web');
  }
  design(): void {
    console.log('design web');
  }
}
const dev = new Developer(); // error: cannot create an instance of an abstract class
const josh = new FrontEndDeveloper();
josh.coding(); // develop web
josh.drink(); // drink sth
josh.design(); // design web

추상 클래스(Abstract Class)는 특정 클래스의 상속 대상이 되는 클래스이며 좀 더 상위 레벨에서 속성, 메서드의 모양을 정의한다. abstract 카워드가 붙으면 상속 받은 클래스에서 추상 클래스 내에 정의된 추상 메서드를 반드시 구현해야 한다.



제네릭

function getText<T>(text: T): T {
  return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);

제네릭이란 타입을 마치 함수의 파라미터처럼 사용할 수 있는 문법으로 <>을 사용하여 변수의 타입을 동적으로 할당할 수 있다. 위 getText 함수처럼 <T>로 파라미터와 리턴 값의 타입을 T로 설정해주면 함수 호출 시 파라미터의 타입에 따라 다르게 사용할 수 있다.

제네릭 타입 매개변수는 관습적으로 Type의 약자인 T를 사용한다.

// #1
const text = getText<string>("Hello Generic");
// #2
const text = getText("Hello Generic");

함수 할당 시에 따로 제네릭 타입을 명시하지 않아도 타입스크립트는 타입을 추론하여 첫 번째 코드와 두 번째 코드가 동일하게 작동하게 하지만, 복잡한 코드에서 타입 추정이 되지 않는 다면 첫 번째 방법을 사용하여 타입을 명시하면 된다.

왜 제네릭 타입을 사용하는가?

function getText(text: any): any {
  return text;
}

여러 인자의 타입을 허용하기 위해서 any 타입으로 설정할 수 있지만, 타입 검사를 하지 않기 때문에 타입스크립트의 이점을 유지할 수 없게 된다. 그렇기 때문에 제네릭 타입을 사용하여 호출 시에 타입을 정의하는 방법을 사용한다.

제네릭 타입 변수

function logText<T>(text: T): T {
  console.log(text.length); // Error: Property 'length' does not exist on type 'T'.
  return text;
}

파라미터의 length를 콘솔에 출력하는 logText 함수의 인자를 제네릭 타입으로 정의할 시, text.length를 사용할 수 있다는 조건이 없기 때문에 에러가 발생하게 된다.

function logText<T>(text: T[]): T[] {
  console.log(text.length);
  return text;
}
function logText<T>(text: Array<T>): Array<T> {
  console.log(text.length);
  return text;
}

이런 경우 위 방법처럼 파라미터의 타입이 배열이라는 것을 정의하고 배열 내 요소를 제네릭 타입으로 정의하여야 에러가 발생하지 않는다.

제네릭 인터페이스

interface GenericLogTextFn {
  <T>(text: T): T;
}
function logText<T>(text: T): T {
  return text;
}
let myString: GenericLogTextFn = logText;

제네릭 타입은 함수뿐만 아니라 인터페이스에도 사용할 수 있는데, 위 코드 처럼 함수 실행 시에 제네릭 타입을 설정하는 방식을 사용할 수 있고,

interface GenericLogTextFn<T> {
  (text: T): T;
}
function logText<T>(text: T): T {
  return text;
}
let myString: GenericLogTextFn<string> = logText;

변수에 할당 시 인터페이스에 제네릭 타입을 설정할 수 있다.

제네릭 클래스

class GenericMath<T> {
  pi: T;
  sum: (x: T, y: T) => T;
}
let math = new GenericMath<number>();

클래스도 인터페이스와 마찬가지로 선언시 클래명 오른쪽에 <T>를 사용하여 프로퍼티나, 메소드에 제네릭 타입을 부여할 수 있다.

제네릭 제약 조건

function logText<T>(text: T): T {
  console.log(text.length); // Error: Property 'length' does not exist on type 'T'.
  return text;
}

제네릭 타입 변수에서 확인하였듯이, 인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 나는데, 해당 타입을 정의하지 않고도 length 속성을 허용하려면 아래 방법을 사용할 수 있다.

interface LengthWise {
  length: number;
}
function logText<T extends LengthWise>(text: T): T {
  console.log(text.length);
  return text;
}
logText({ length: 0, value: 'hi' });

length의 타입을 설정하였기 때문에 에러가 발생하지 않고 length라는 key를 가지고 있는 객체를 파라미터로 넣어 실행해도 해당 값이 콘솔에 출력된다.



마치며

오늘은 타입 정의에 사용되는 문법(연산자, 제네릭)과 클래스에 대해서 정리하였다. 클래스는 자바스크립트와 다른 점이 많지 않아서 자세히 정리하지는 않았지만 추후 tsconfig 설정에 관련된 부분에 다시 한번 다룰 예정이다. 타입 정의와 관련된 주요 문법을 살펴보면서 타입스크립트에 타입 지정에 다양한 문법이 있다는 것을 알았고, 익숙해질 수 있도록 여러 예시를 살펴보고 연습해볼 필요가 있다고 느꼈다. 다음 파트에서는 타입 추론, 호환, 별칭에 대한 내용을 정리할 예정이다.

profile
개발자로 성장하기

0개의 댓글