TypeScript 시작하기 1

1. 타입스크립트란 & 설치

Tyepscript란

타입스크립트는 자바스크립트 슈퍼셋입니다. 새로 만들어진 언어가 아닌 자바스크립트 언어를 사용해 새로운 기능과 장점을 추가한 언어로, 자바스크립트 환경인 브라우저나 node.js 또한 타입스크립트를 실행 할 수 없습니다. 즉, 기본적으로 자바스크립트를 실행하는 환경에서는 타입스크립트가 지원되지 않습니다.

타입스크립트는 프로그래밍언어이지만 도구에 가깝습니다. 코드를 실행하여 타입스크립트 코드를 자바스크립트로 컴파일 하여 실행합니다. 자바스크립트 런타임에서 에러가 발생하기 전에 코드의 에러를 개발자들이 미리 알 수 있도록 해서 개발 중에 발생하는 에러를 초기에 발견하고 수정 할 수 있도록 합니다.

다음은 코드 예시 입니다.

function sum(num1, num2) {
  return num1 + num2;
}

console.log(sum("1", "2"));

만약 위와 같이 숫자를 더해서 반환하는 함수에 문자열을 더해도 결과값(12)이 나옵니다. 이 값은 의도하지 않는 값으로 문자열을 넣었을때는 오류를 반환 해야합니다. 하지만, 자바스크립트에서는 기술적인 에러가 아니기 때문에 에러가 발생하지는 않겠지만, 논리적으로 잘못된 부분이고 숫자값을 더하는 함수는 숫자값만을 input으로 받고 output해줘야합니다.

이 부분은 조건문(if)으로 에어를 검사할 수 있지만, 개발자는 이런 모든 작업들을 포착하고 처리 하기는 힙니다. 그렇기 때문에 유효하지 않는 코드들을 타입스크립트로 방지할 수 있습니다.

타입스크립트 설정

타입스크립트도 결국 자바스크립트로 컴파일하여 실행하기 때문에 node.js를 설치한 환경해서 실행 해야 합니다.



2. 타입

Number, String, Boolean

// JavaScript
function sum(num1, num2) {
  return num1 + num2;
}

const number1 = 5;
const number2 = 2.8;
const number3 = "5";

console.log(sum(number1, number2));
// 7.8
// number
console.log(sum(number2, number3));
// 52.8
// string

숫자와 숫자를 더할때는 문제가 없지만 JS에서 숫자와 문자열을 더할 경우 타입을 문자열로 변환하여 “52.8”이 출력이 된다.

// TypeScript
function sum(num1, num2) {
  return num1 + num2;
}

const number1 = 5;
const number2 = 2.8;
const number3 = "5";

console.log(sum(number1, number2));
// = 7.8
// number
console.log(sum(number2, number3));
// error

반면에 타입스크립트에서는 잘못된 타입을 입력할 경우 다음과 같이 에러를 출력한다.

사전에 이러한 오류를 방지하기 위해 TS에서는 타입 배정(type assignment)를 추가할 수 있다.

// TypeScript
function sum(num1: number, num2: number) {
  return num1 + num2;
}

const number1 = 5;
const number2 = 2.8;
const number3 = "5";

console.log(sum(number1, number2));
// = 7.8
// number
console.log(sum(number2, number3));
// error

그렇게 되면 기존에 number3에서 IDE에서 에러를 보여준다.

브라우저에서는 내장 타입스크립트 지원이 없기 때문에 런타임에서 자바스크립트가 다른 식으로 작동하도록 변경하지 않는다. 타입스크립트 코드를 자바스크립트 코드로 컴파일 하기 전까지, 즉 개발 도중에만 유용하지만 이는 추가적인 단계와 추가적인 온정성 검사를 추가하기 때문에 아주 유용하다.

타입 추론

기본적으로 TypeScript는 타입 추론이라는 것을 한다.

// TypeScript
const number1 = 5;
const boolean1 = false;
const string1 = "5";

let number2 = 7;
number2 = "str"; // err

이코드에서 number1, boolean1, string1의 경우 타입을 명시하지 않았지만 에러가 나지 않는다.

그 이유는 TS에서 자동으로 타입을 추론해서 해당 타입이 무엇인지 알기 때문이다.

상수의 경우에는 값이 안변하닌 2번과 같이 상수가 아닌 변수를 선언하고 값을 할당하려고 할 경우 해당 값에서 오류를 보여주게 된다.

Object

const person: object = {
	name : 'Lee'
	age : 30
};

객체의 경우 obejct로 선언하는 것이 아닌 다음과 같이 키 타입을 추가해야한다.

const person: {name: string; age: number;} = {
	name : 'Lee'
	age : 30
};

Array

배열 또한 해당 내용들의 타입들을 정의해 주어야한다.

const person: string[] = {
  hobby: ["sprots", "game"],
};

위와 같이 내용물의 string타입일 경우 string[]으로 정의해준다.

Tuple

튜플은 배열과 비슷하지만 길이와 타입이 고정된 배열이다.

const role: [number, string] = [2, "person"];

const role: number[] = [2, "person"]; // error
const role: string[] = [2, "person"]; // error

다음과 같이 있을때 role에는 첫번째 값은 number 타입, 두번째 값은 string 타입의 형식이 오게 할때 사용한다.

이와 같이 코드를 작성하게 되면 해당 배열에 해당 타입 순서에 맞지 않게 코드를 넣게 되면 오류가 난다.

const role: [number, string] = [2, "person"];
role[1] = 10; // error : string이 있는 부분에 number를 넣을 수 없다.

role = []; // 빈배열도 error가 나오며, 무조건 해당 타입 구조와 일치해야한다.

하지만 여기에는 약간의 예외가 있다.

role.push(”Hello”)를 하게 되면 number가 와야하는 차례이지만 push의 경우 강제성의 해당하지 않는다.

Enum

enum은 열거형 타입으로 JS에는 없는 TS에만 있는 개념인데 열거형 목록을 제공한다.

자바스크립트에서 열거형 목록을 사용할때에는 다음과 같이 사용한다.

예시로 person에 관리자랑 일반 사용자를 각각 0, 1로 구분하고 조건문으로 해당 값을 구분한다 했을때 다음과 같아 진다.

const admin = 0;
const user = 1;
const user2 = 2;
const user3 = 3;
const user4 = 4;

person = {
	name : "lee",
	role : 0;
}

person = {
	name : "kim",
	role : 1;
}

if(person.role === 0) reutrn console.log("관리자");
else return console.log("일반 유저");

위의 코드를 typescript에서는 enum을 사용해서 해결할 수 있다.

또한 이때 user에 등급의 여러가지로 나누어 진다고 하면 일일이 해당 값을 정의해 줘야 하는데 이때 enum을 사용하면 효과적이다.

enum Role {admin, user, user2, user3, user4}

person = {
	name : "lee",
	role : Role.admin
}

person = {
	name : "kim",
	role : ROLE.user
}

if(person.role === Role.admin) reutrn console.log("관리자");
else return console.log("일반 유저");

위에서 설정한 것처러럼 ADMIN은 0, USER는 1순으로 값이 할당 된다.

그리고 0부터 시작하는게 아니라아니라 다음과 같이 임의이 값을 할당 할 수 있다.

enum Role {
  admin = 5,
  user,
  user2,
  user3,
  user4,
}

이때에는 뒤에 값은 5,6,7 … 순으로 순서대로 증가한다.

물론 enum Role {admin = 5, user = 20, user2 = 100 } 와 같이 각각 모든 값을 할당 하는 것도 가능하다.

그리고 숫자만 열거형으로 할당하는게 아닌 다음과 같이 문자열도 가능하고 혼합도 가능하다.

enum Role {
  admin = "ADMIN",
  user = 20,
  user2 = 100,
}

Any

any타입은 모든 종류의 값을 저장할 수 있다.

const num: any = "1"; // true
const str: any = 1; // true
const arr: any = ["1", 1, 2]; // true

하지만 타입스크립트 추론이 제 기능을 하도록 하거나 타입을 정확하게 설정해야 되며 필요하지 않는 다면 any를 사용하지 않도록 해야한다.

Union 타입

function combie(input1: number | string, input2: number | string) {
  return input1 + input2;
}

console.log(combie(3, 2)); // 5 : true
console.log(combie("Hello", "World")); // HelloWorld : true
console.log(combie("Hello", 1)); // Hello1 : true
console.log(combie("Hello", true)); // Error

| 기호를 통해 여러가지 타입을 받을 경우를 할 수 있다.

Literal 타입

숫자나 문자열도 아닌 정확한 값을 타입이다.

예를 들면 위의 Union값에서는 input값으로 string + number의 조합으로 값을 받아도 해당 값이 에러가 나지 않고 JavaScript로 받아 지게 되었다. 그래서 아래와 같이 해당 값을 같은 타입인 경우만 더하는 코드가 있다.

function combie(input1: number | string, input2: number | string){
	if(typeof input1 === 'number' && typeof input2 ==== 'number'){
		return +input1 + +input2; // +를 붙이면 숫자가된다. parseInt()롤 바꾸는 것과 비슷
	}else {
		return input1.toString() + input2.toString();
	}
}

console.log(combie(3, 2)); // 5 : true
console.log(combie("Hello", "World")); // HelloWorld : true
console.log(combie("Hello", 1)); // ERROR

위의 코드를 통해서 문자 + 숫자 조합을 방지할 수 있다.

여기에 더해서 리턴 값에서 사용자가 어떠한 방식으로 변환되서 반환하게 하려는 경우, 예를 들어 string으로 반환하려한다면 다음과 같이 resultType으로 값을 따로 받아서 지정할 수 있다.

function combie(input1: number | string, input2: number | string, resultType: string){
	if(typeof input1 === 'number' && typeof input2 ==== 'number' || resultType === "as-number"){
		return +input1 + +input2;
	}else {
		return input1.toString() + input2.toString();
	}
}

console.log(combie(3, 2, "as-number")); // 5 : true
console.log(combie("Hello", "World", "as-text")); // HelloWorld : true

// 아래의 값은 as-number이기 때문에 문자열 "1"를 숫자로 바꾸어서 더한 결과 값이 나온다.
console.log(combie("1", 1, "as-number")); // 2 :

이렇게 함으로써 “1” + 1을 계산하게 만들 수 있다.

하지만 이 방법의 단점은 해당 값인 ‘as-number’, ‘as-text’를 기억해야 한다.

그래서 다음과 같이 리터럴 타입을 사용해서 기억을 하지 않아도 어떤 값이 들어가는지를 알 수 있게 할 수 있다.

function combie(input1: number | string, input2: number | string, resultType: 'as-number' | 'as-text'){
	if(typeof input1 === 'number' && typeof input2 ==== 'number' || resultType === "as-number"){
		return +input1 + +input2;
	}else {
		return input1.toString() + input2.toString();
	}
}

console.log(combie(3, 2, "as-number")); // 5 : true
console.log(combie("Hello", "World", "as-text")); // HelloWorld : true

// 아래의 값은 as-number이기 때문에 문자열 "1"를 숫자로 바꾸어서 더한 결과 값이 나온다.
console.log(combie("1", 1, "as-number")); // 2 :

리터럴 타입은 resultType: 'as-number' | 'as-text' 와 같이 아무 string이 아닌 특정 string값만 받게 할 수 있다.

이렇게 할때 조건문 안에 if(typeof input1 === 'number' && typeof input2 ==== 'number' || resultType === "as-num") 과 같이 틀리게 쓸 경우 에러를 발생 시켜 준다.

Aliases 타입

aliases 타입은 반복되는 타입을 저장 해 둘 수 있다.

type DataTypes = number | string
type resultTypes = "as-number" | 'as-text'
type User = { name: string; age: number };

function Data(input1: DataTypes, input2: DataTypes, resultType: resultTypes){
	...
}
const u1: User = { name: 'Lee', age: 20 };

Return 타입

반환된 값의 타입 number값을 두개 받아서 더한 값을 return 할 경우 typescript는 반환 값도 number라는 추론을 한다.

// 반환값을 따로 지정하지 않고 타입스크립트에서 추론한 결과
// true
function sum(num1: number, num2, number) {
  return num1 + numb2;
}

// 반환값 타입 따로 지정할 경우 결과
// true
function sum(num1: number, num2, number): number {
  return num1 + numb2;
}
// false
function sum(num1: number, num2, number): string {
  return num1 + numb2;
}

위와 같이 올바른 타입의 반환 값을 지정할 경우 정상적으로 작동하지만 number가 반환 될 수 밖에 없는데도 string을 반환 값으로 지정할 경우 Error가 난다.

void 타입

Return 타입에는 void 타입이 있다.

function sum(num1: number, num2, number): number {
  return num1 + numb2;
}

function printResult(num: number): void {
  console.log("Result: " + num);
}

printResult(sum(1, 2)); // Result: 3

이때 printResult()는 아무것도 반환하지 않기때문에 void 타입을 가진다.

이 아무것도 반환하지 않는 함수를 console.log(printResult())를 찍게 되면 undefined를 반환한다.

기존에 undefined랑 다른 것이 타입스크립트에는 값을 지정하지 않는 경우에 undefined가 나오기 때문이다. 즉, 함수 반환값이 없다고 undefined를 넣게 되면 반환문이 있을 것이라고 여기게 된다.

만약 다음과 같이 return;을 쓰게 되면 실행을 할 수 있게 된다.

// true
function printResult(num: number): void {
  console.log("Result: " + num);
}

// false
function printResult(num: number): undefined {
  console.log("Result: " + num);
}

// true
function printResult(num: number): undefined {
  console.log("Result: " + num);
  return;
}

정리하자면 void는 반환값이 없을때 사용하고 undefined는 반환되는 값이 없을때 사용한다.

Function 타입

function sum(num1: number, num2, number): number {
  return num1 + numb2;
}

let func1: Function;
func1 = sum;
func1 = 5; // error

만약 function 타입을 지정하지 않을 경우 IDE에서 func1 = 5를 했을때 실행전에 에러를 발견할 수 없게 된다.

화살표 함수로 반환 타입이 지정된 Function 타입을 지정 할 수 있다.

function sum(num1: number, num2, number): number {
  return num1 + numb2;
}

let func2: () => number;
func2 = sum;

또한 파라미터 값(매개변수)를 추가해줄 수 있는데 원래 함수의 파라미터명과 일치하지 않아도 된다.

이 와같이 파라미터값또한 지정하게 되면 아래 처럼 사전에 에러를 잡을 수 있다.

function sum(num1: number, num2, number): number {
  return num1 + numb2;
}

function sum2(num1: string, num2, string): string {
  return num1 + numb2;
}

let func3: (a: number, b: number) => number;
func3 = sum; // true
func3 = sum2; // false

func3는 파라미터가 number값을 가져야하기 때문에 파라미터가 stringsum2로는 값을 지정할 수 없다.

callBack 타입
function sum(num1: number, num2, number, cb: (num: number) => void) {
  const result = num1 + numb2;
  cb(result);
}

sum(10, 20, (result) => {
  console.log(result);
});

// false : 인자가 하나인 값을 반환한다.
sum(10, 20, (result, result2) => {
  console.log(result);
});

여기서 cb: (num: number) => void으로 void를 Retrun 타입으로 지정하면서 기본적으로 여기서 반환하게 되는 모든 결과를 무시하게 된다. (return으로 값 자체는 반환 할 수 있긴하다.)

무언가를 반환할 수 있긴 하지만 반환되는 값으로는 아무 작업도 수행하지 않는다고 callback함수 타입에 정의되어 아무 작업도 하지 않게 된다.

unknown 타입

unknown 타입은 알 수 없는 타입이다.

타입스크립트에서 기본 제공되는 any와는 타입으로 무엇을 입력할지 않수 없는 타입이다. (any보다 좀더 제한적으로 사용한다.)

에러 발생 없이 어떤 값이든 저장할 수 있고 허용한다.

// any를 사용할때
let value: any;
let name: string;

value = 5;
value = "Lee";
name = value;

// unknown을 사용할때
let value: unknwon;
let name: string;

value = 5;
value = "Lee";
name = value; // error

위와 같이 unknwon을 사용하는 경우 value에 현재 저장되어 있는 타입을 확인해야지 문자열을 원하는 변수에 할당할 수 있게 된다.

let value: unknwon;
let name: string;

value = 5;
value = "Lee";

if (typeof value === "string") {
  name = value; // true
}

if문에서 valuestring이라고 설정했기 때문에 할당이 될 수 있게 된다.

즉, 추가적인 타입 검사를 필요로 한다.

Never 타입

아무것도 반환하지 않는 void와 달리 never타입은 함수가 반환할 수 있는 타입이다.

function ErrorFunc(msg: string, code: number){
	thorw {message: msg, errorCode: code};
}

ErrorFunc("Error !!!", 500);

위와 같이 실행시키게 되면 error를 발생시키게 되고 아무것도 반환하는 것이 아닌게 된다.

정확히는 이 함수는 never를 반환하며 반환값을 생성하지 않는다.

function ErrorFunc(msg: string, code: number){
	thorw {message: msg, errorCode: code};
}

const result = ErrorFunc("Error !!!", 500);
console.log(result);

try~catch문을 사용하여 계속 진행할 수 있지만 이 함수가 스크립트와 항상 출동하기 때문에 아무것도 반환하게 되지 않는다.

따라서 이 함수는 void뿐만 아니라 never도 가능하게 된다.



3.OOP - class

객체 지향 프로그래밍 Object-oriented Progamming (OOP)

객체 지향 방법으로 객체를 구성하면서 앱에서 로직을 로직의 일부를 관리하는 객체로 분할 할 수 있다.

데이터를 저장하고 메서드를 실행하기 위해 메서드를 저장하는 데 사용하는 데이터 구조로 객체의 청사진이다.

클래스를 사용하여 객체의 형태, 포함해야하는 데이터, 클래스를 기반으로 객체를 쉽게 만들 수 있으려면 어떤 메서드가 필요한지 정의할 수 있기 때문에 이를 클래스 내의 ‘인스턴스’라고한다.

따라서 객체는 클래스 내의 인스터스인 것이다.

이러한 클래스를 기반으로 하면 동일한 구조, 동일한 클래스를 기반으로 하는 동일한 메서드로 여러 객체를 빠르게 복제할 수 있다.

이처럼 클래스는 객체의 형태, 포함해야할 속성과 메서드를 정의하는데에 도움이 된다.

Class

클래스 생성

class Department {
  name: string;
}

클래스명은 관례상 첫글자를 대문자로 한다.

변수의 경우 let이나 const없이 입력한다.

예시로 name이라는 변수를 선언했는데 이것은 객체는 아니다.

객체는 키-값 쌍으로 되어 있어햐하는데 키에 해당하는 값만 선언한다.

이 것을 클래스의 필드라고 한다.

그리고 name에 Default값으로 기본값을 초기화(name: string = “Lee”)할 수 있지만 굳이 사용하지 않아도 된다.

생성자

class Department {
  name: string;

  // 생성자
  constructor(n: string) {
    this.name = n;
  }
}

기본적으로 클래스에는 constructor라는 생성자 메서드가 예약어로 있다.

이를 사용하여 객체에 대한 초기화 작업을 수행할 수 있다.

this 키워드로 내부의 변수인 name에대가 n이라는 값을 받아서 값을 초기화 할 수 있다.

Department 객체를 생성할때 n값을 받아서 constructor를 통해 name값을 초기화 한다.

  1. 클래스로 객체 생성

    class Department {
      name: string;
    
      constructor(n: string) {
        this.name = n;
      }
    }
    
    const Accounting = new Department("Accounting");
    console.log(Accounting); // Department {name : "Accounting"}
    

    new키워드를 통해 Department객체를 생성할 수 있다.

    이때 new 키워드가 있는 클래스를 실행할때 constructor(생성자)가 호출되고 n이라는 값을 전달 해주어야한다.

클래스 method 생성 및 this

class Department {
  name: string;

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

  describe() {
    console.log("deparment: " + this.name);
  }
}

const Accounting = new Department("Accounting");
accounting.describe(); // Department {name : "Accounting"}

클래스에 있는 함수를 method라고 하며 메서드의 경우에는 function 예약어 없이 만들면 된다.

이때 클래스 내부에 있는 변수값을 사용할때는 this 키워드를 사용한다.

실행할때는 객체를 실행하는 것처럼 호출하면 된다.

여기서 재밋는 점은 해당 메서드를 복사할 경우 이다.

const accountingCopy = { describe: accounting.describe };
accountingCopy.describe();

accouting에서 describe 함수로 호출한 값을 복사한 변수 accountingCopy가 있을때 해당 함수에서 describe(); 를 실행하면 undefined가 나온다.

그 이유는 accountingCopy에서 describe를 호출하기 때문에 accountingCopy를 참조하고 이 accountingCopyname 속성이 없는 객체라서 this.name에 접근하게 되면 에러가 발생한다.

그래서 이문제를 해결하기 위해 describe 메서드에 매개변수를 추가할 수 있다.

이때 복사하는 변수에 name값도 같이 추가해주어야한다.

class Department {
  name: string;

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

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }
}

const accountingCopy = { name: "Kim", describe: accounting.describe };
accountingCopy.describe();

Private & public 제어자

class Department {
  name: string;
  employees: string[] = [];

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

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  // emplyess를 추가하는 메서드
  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  // emplyess를 출력하는 메서드
  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

const Accounting = new Department("Accounting");

accounting.addEmplyee("Lee");
accounting.employees[1] = "Kim";

accounting.printEmplyeeInformation(); // ["Lee", "Kim"]

위와 같이 emplyees라는 변수가 있고 해당 변수에 값을 추가하는 메서드인 addEmplyee와 해당 변수값을 출력하는 printEmplyeeInformation 메서드가 있다.

addEmplyee를 통해서 원하는 값을 emplyees에 추가할 수 있는데, 이때 accounting.employees[1] = 'Kim'; 으로 원치 않는 방식으로 값을 추가할 수도 있다.

이때 private 키워드를 통해서 해당 방식을 막고 의도하는 방식으로만 값을 추가하게 할 수 있다.

의도하지 않는 방식을 사용하게 되면, 코드 컨벤션이나 여러 이슈들이 생겼을때 문제가 생길수 있기 때문에 의도 되로 명확하게 동작하도록 해야한다. 만약 addEmplyee가 단순히 배열에 값을 추가하는 것 이상의(예시로 유효성 검사를 추가한다던가 하는.. ) 작업을 수행하할 수도 있기 때문에 통일된 방법을 쓰게 해야한다.

class Department {
  name: string;
  private employees: string[] = [];

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

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  // emplyess를 추가하는 메서드
  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  // emplyess를 출력하는 메서드
  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

const Accounting = new Department("Accounting");

accounting.addEmplyee("Lee");
accounting.employees[1] = "Kim"; // Error

위와 같이 emplyeesprivate으로 선언해두면 해당 값을 class 외부에서 접근할 수 없게 된다.

이러한 private 키워드를 제어자라고 하면 기본값으로 있어서 추가할 필요 없는 public 키워드도 있다. (public은 외부에서 접근 가능하다는 것이다.)

약식 필드

필드에 값을 일일이 추가하고 생성자를 통해 초기화를 할 때 반복적인 작업이 많아진다.

이때 필드명과 생성자의 명을 동일시하게 하여 초기화를 해주고 접근자를 적어주면 위에서 했던 방식과 같이 해당 값들을 초기화 할 수 있다.

class Department {
  constructor(public name: string, private employee: string) {}

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  // emplyess를 추가하는 메서드
  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  // emplyess를 출력하는 메서드
  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

물론 내부적으로 호출할 때에는 this 키워드를 통해 this.name으로 호출한다.

이렇게 축약된 코드로 코드의 양을 줄일 수 있다.

readonly 제어자

private이나 public이어서도 안되고, 초기화 이후 변경되면 안되는 특정 필드에 경우는 readonly로 추가해주면 된다.

해당 객체를 구분하는 유일한 고유 id값이 있다고 할때 예시를 들면 아래와 같다.

class Department {
  private employee: string;
  // priavate readonly id: string;

  constructor(private readonly id: string, public name: string) {
    // this.id = id;
  }

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  // emplyess를 추가하는 메서드
  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  // emplyess를 출력하는 메서드
  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

이렇게 선언된 id값은 초기화 중에 한번만 사용할 수 있다.

상속

위에서 만든 Department라는 부서가 있고 특정 부서에 고유 속성과 메서드를 가지는 개별 부서를 생성할때 extends 키워드로 상속을 사용한다.

상속은 하나의 클래스에서만 상속할 수 있고 여러 클래스를 동시에 상속할 수 없다.

class ITDepartment extends Department {}

const IT = new ItDepartment("IT 001", "Accounting");

위와 같이 Department를 상속하면 Department의 생성자를 그대로 사용할 수 있다.

그리고 해당 값에 고유 생성자를 추가할 수 있다.

class ITDepartment extends Department {
  admins: string[];

  constructor(id: string, admins: string[]) {
    super(id, "IT");
    this.admis = admins;
  }
}

const it = new ItDepartment("IT 001", ["Lee"]);

it.addEmployee("Lee");
it.addEmployee("Kim");

it.describe();
it.printEmplyeeInformation();

이때 부모의 생성자의 필드를 그대로 사용하려면 super 키워드를 사용한다.

그러면 super는 부모 생성자의 인수인 id와 ㄷ을 취하므로 id값을 super로 전송하고 name값을 하드 코딩할 수 있다.

admins는 부모 클래스의 Deaprtment에 없는 것이기 때문에 ITDepartment에 추가한다.

상속하여 메서드 추가하기

기존 메서드를 그대로 사용하면서 상속받은 클래스에서만 사용하는 메서드 추가

class AccountingDepartment extends Department {
  constructor(id: string, private reports: string[]) {
    super(id, "IT");
  }

  addReport(text: string) {
    this.reports.push(text);
  }

  printReports() {
    console.log(this.reports);
  }
}

const accounting = new AccountingDepartment("IT 002", []);

accounting.addReport("Add Reports ...");
accounting.printReports(); // [Add Reports ...]
상속 재정의

기존에 있던 부모 클래스의 메서드를 재정의

// 부모 클래스
class Department {
  protected employee: string; // private -> protected
  // priavate readonly id: string;

  constructor(private readonly id: string, public name: string) {
    // this.id = id;
  }

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  // emplyess를 추가하는 메서드
  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  // emplyess를 출력하는 메서드
  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

// 하위 클래스
class AccountingDepartment extends Department {
  constructor(id: string, private reports: string[]) {
    super(id, "IT");
  }

  addEmployee(name: string) {
    // name이 "Lee"가 아닐 경우 추가
    if (name === "Lee") {
      return;
    }
    this.emplyees.push(name);
  }
}

const accounting = new AccountingDepartment("IT 002", []);

accounting.addEmployee("Kim");

private employeeprivate이기 때문에 Department에서만 사용가능하다.

접근할 수 있도록 하면서 외부에서 변경 불가능한 속성으로 만들려고 한다면 protected로 바꾸면 된다.

protectedprivate와 비슷하지만 상속 받은 클래스에서도 사용가능하게 한다.

getter & setter

getter
class AccountingDepartment extends Department {
  private lastReport: string;

  constructor(id: string, private reports: string[]) {
    super(id, "IT");
    this.lastReport = reports[0];
  }

  addReport(text: string) {
    this.reports.push(text);
    this.lastReport = text;
  }
}

위에서 lastReport 필드는 private로 지정되어 있어서 이 메서드 내에서 접근은 가능하지만 점(.)을 표기하여 접근하는 것은 불가능하다.

이때 값을 가지고 올때 함수나 메서드를 실행하는 속성인 getter를 통해 접근 가능하도록 할 수 있다.

class AccountingDepartment extends Department {
  private lastReport: string;

  // 해당 값으로 this.lastReport를 반환하게 할 수 있다.
  get mostRecentReport1() {
    return this.lastReport;
  }

  // this.lastReport값이 존재하는지 확인하고 반환한다.
  get mostRecentReport2() {
    if (this.lastReport) {
      return this.lastReport;
    }
    throw new Error("값이 존재하지 않습니다.");
  }

  constructor(id: string, private reports: string[]) {
    super(id, "IT");
    this.lastReport = reports[0];
  }

  addReport(text: string) {
    this.reports.push(text);
    this.lastReport = text;
  }
}

const accounting = new AccountingDepartment("IT 002", []);

console.log(accounting.mostRecentReport); // O
console.log(accounting.mostRecentReport()); // X

메서드를 실행하는 것처럼 accounting.mostRecentReport()이 아닌 accounting.mostRecentReport으로 호출한다.

setter
class AccountingDepartment extends Department {
  private lastReport: string;

  // this.lastReport값이 존재하는지 확인하고 반환한다.
  get mostRecentReport2() {
    if (this.lastReport) {
      return this.lastReport;
    }
    throw new Error("값이 존재하지 않습니다.");
  }

  set mostRecentReport(value: string) {
    this.addReport(value);
  }

  set mostRecentReport(value: string) {
    if (!value) {
      throw new Error("값을 확인하세요.");
    }
    this.addReport(value);
  }

  constructor(id: string, private reports: string[]) {
    super(id, "IT");
    this.lastReport = reports[0];
  }

  addReport(text: string) {
    this.reports.push(text);
    this.lastReport = text;
  }
}

const accounting = new AccountingDepartment("IT 002", []);

accounting.mostRecentReport = ""; // Error
accounting.mostRecentReport = "setter text";

console.log(accounting.mostRecentReport);

정적 메서드 & 속성

정적 메서드와 속성으로 클래스의 인스턴스에 접근할 수 없는 속성과 메서드를 클래스에 추가할 수 있다.

새 클래스 이름을 먼저 호출하지 않고 클래스에 직접 추가하는 것이다.

JS에 에 내장된 예시를 보면 Math생성자 함수나 클래스를 Math.PI, Math.Pow()와 같이 접근할 수 있다. Math의 인스턴스에 접근할 수 없는 메서드와 속성이므로 new Math를 호출하여 사용하지 않는다.

호출할 경우 작동하지 않고 직접 클래스 자체에서 속성과 메서드에 접근해야한다.

class Department {
  private employee: string;
  static fiscalYear = 2022;

  constructor(private readonly id: string, public name: string) {
    // console.log(this.fiscalYear); -> 작동안함
    console.log(Deoartment.fiscalYear);
  }

  static createEmployee(name: string) {
    return { name: name };
  }

  describe(this: Department) {
    console.log("deparment: " + this.name);
  }

  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

const employee1 = Deparment.createEmployee("Lee");
console.log(employee1); // {name : "Lee" }
console.log(Department.fiscalYear); // 2022

추상 클래스

Department 클래스를 ITDepartment가 상속할때 위에서 했던 것처럼 재정의를 할 수 있지만, 해당 메서들르 재정의하는 옵셕을 입력하고 싶지 않을때나, 특정 클래스를 확장시킬때 특정 메서드를 구현하거나 재정의하도록 하게 하는 경우가 있다.

이때 abstract를 사용하여 추상화 할수 있다.

해당하는 메서드 앞에 붙일 수 있고, 하나라도 해당 키워드를 쓰면 class 앞에도 사용해야 한다.

abstract class Department {
  private employee: string;
  static fiscalYear = 2022;

  constructor(private readonly id: string, public name: string) {
    // console.log(this.fiscalYear); -> 작동안함
    console.log(Deoartment.fiscalYear);
  }

  static createEmployee(name: string) {
    return { name: name };
  }

  abstract describe(this: Department): void;

  addEmployee(employee: string) {
    this.employees.push(emplyee);
  }

  printEmplyeeInformation() {
    console.log(this.emplyees);
  }
}

class ITDepartment extends Department {} // Error
class ITDepartment extends Department {
  describe(this: Department) {
    console.log("Hello World");
  }
}

추상화 키워드를 사용할 경우 해당 부분을 구현하지 않으면 Error가 발생한다.

싱글톤 & 개인 생성자

OOP에 있는 싱글톤 패턴은 특정 클래스의 인스턴스를 정확하게 하나만 갖도록 한다.

IT 부서는 하나 이상일 수 있지만 회계 부서는 하나로 구현해야 할때

new AccountingDepartmnet를 여러번 수동으로 호출하지 않기 위해서 AccountingDepartmnet 생성자 앞에 preivate 키워드를 붙인다.

class AccountingDepartment extends Department {
  private lastReport: string;

  get mostRecentReport() {
    if (this.lastReport) {
      return this.lastReport;
    }
    throw new Error("값이 존재하지 않습니다.");
  }

  private constructor(id: string, private reports: string[]) {
    super(id, "IT");
    this.lastReport = reports[0];
  }

  addReport(text: string) {
    this.reports.push(text);
    this.lastReport = text;
  }
}
const accounting = new AccountingDepartment("d2", []); // Error

이렇게 하면 new 키워드를 하여 호출할때 error가 나온다.

그래서 클래스 자체에서 정적 메서드를 호출하게 하여 인스턴스화할 필요가 없게한다.

class AccountingDepartment extends Department {
  private lastReport: string;
  private static instance: AccountingDepartment;

  get mostRecentReport() {
    if (this.lastReport) {
      return this.lastReport;
    }
    throw new Error("값이 존재하지 않습니다.");
  }

  private constructor(id: string, private reports: string[]) {
    super(id, "IT");
    this.lastReport = reports[0];
  }

  static getInstance() {
    if (AccountingDepartment.instance) {
      return this.instance;
    }
    this.instance = new AccountingDepartmnet("d2", []);
    return this.instance;
  }

  addReport(text: string) {
    this.reports.push(text);
    this.lastReport = text;
  }
}

const accounting = AccountingDepartment.getInstance();
const accounting2 = AccountingDepartment.getInstance();