TypeScript 시작하기 2

4. interface

interface

인터페이스는 객체의 구조를 설명하는데에 사용되고 interface는 JS는 없고 TS에만 있다.

interface Person {
	name: string;
	age: number;
	greet(phrase: string): void;
}

let user1: Person
user1 = {
	name: "Lee",
	age: 27,
	greet((phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

user1.greet("Hi I am")

인터페이스와 type과 차이점은 객체의 구조를 설명하기 위해서만 사용한다는 것이다.

interface Greetable{
	name: string;
	greet(phrase: string): void;
}

let user1: Greetable
user1 = {
	name: "Lee",
	age: 27, // interface에 age가 없기 때문에 Error
	greet(phrase: string) {
		console.log("phrase" : this.name)
	},
}

user1.greet("Hi I am")

→ 인터페이스가 마련한 약속을 이행해야한다.

클래스에서 상속은 한 클래스로 부터만 상속할 수 있지만 인터페이스는 여러개를 받을 수 있다.

interface Greetable {
  name: string;
  greet(phrase: string): void;
}

interface AnotherInterface {
  age: number;
}

class Person implements Greetable, AnotherInterface {}

그리고 interface에 없는 name에 대한 값을 받아야한다.

interface Greetable{
	name: string;
	greet(phrase: string): void;
}

class Person implements Greetable{
	name: string;
	age: 30;

	construcor(n: string){
		this.name = n;
	}
	greet(phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

let user1: Greetable;
user1 = new Person("Lee");

인터페이스는 주로 구체적인 구현이 아닌 서로 다른 클래스 간의 기능을 공유하기 위해 사용된다.

인터페이스 내의 구현이나 값을 입력하는 것이 아닌 구조와 클래스가 가져야 할 기능을 입력해야한다.

추상 클래스로 작업하는 것과 비슷하지만 인터페이스에는 구현 세부 사항이 전혀 없는 반면, 추상 클래스는 엎어써야 했던 부분과 구체적 구현 부분을 혼합할 수 있다.

인터페이스가 유용한 이유

interface Greetable{
	name: string;
	greet(phrase: string): void;
}

class Person implements Greetable{
	name: string;
	age: 30;

	construcor(n: string){
		this.name = n;
	}
	greet(phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

let user1: Greetable;
user1 = new Person("Lee");

Greetable 인터페이스의 greet를 클래스에 메서드로 가지고 있고 다른 클래스에도 가지고 있는지 확인할때 사용한다.

그러면 클래스 간에 기능을 쉽게 공유할 수 있는데 모든 클래스는 메서드가 호출될때 실행되어야 하는 정확한 코드, 고유한 구현을 추가해야한다.

인터페이스 타입
interface Greetable{
	readonly name: string; // O 읽기 전용
	public age: number // X pulic, private 키워드 사용 불가
}

class Person implements Greetable{
	name: string;
	age: 30;

	construcor(n: string){
		this.name = n;
	}
	greet(phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

user1 = new Person("Lee");
user1.name = "Kim" // Error

name은 읽기 전용이기 때문에 Error가 나온다.

인터페이스를 클래스에 구현하고 나면 인

class에서 readonly를 추가하지 않았음에도 클래스에서는 Greetable를 구현해야한다는 것을 인지하고 name 속성이 읽기 전용임을 자동으로 추론한다.

인터페이스 확장 (class 사용하기 =)

인터페이스를 통해서 다음과 같이 Person 클래스에 Named를 따로 추가할수 있다.

interface Named {
	readonly name: string;
}

interface Greetable{
	greet(phrase: string): void;
}

class Person implements Greetable, Named{
	name: string;
	age: 30;
}
...

만약 GreetableNamed 인터페이스가 항상 필요하다면 다음과 같이 extends 키워드로 추가하여 인터페이스를 확장하여 사용할 수 있다.

interface Named {
	readonly name: string;
}

interface Greetable extends Named{
	greet(phrase: string): void;
}

class Person implements Greetable{
	name: string;
	age: 30;

	construcor(n: string){
		this.name = n;
	}
	greet(phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

user1 = new Person("Lee");
user1.name = "Kim" // Error

그리고 인터페이스 확장또한 쉼표를 통해서 여러개를 확장시킬 수 있다.

interface Greetable extends Named, AnotherInterface {
  greet(phrase: string): void;
}

클래스의 경우 이렇게 구현할 수 없다.

클래스와 상속을 사용하는 경우 하나의 클래스로부터만 상속할 수 있고 다수의 클래스로부터는 상속할 수 없지만 인터페이스는 다수의 인터페이스로부터 상속받을 수 있다.

함수 타입에서의 인터페이스

인함수에서 사용하기

type AddFunc = (a: number, b: number) => number;

let add: AddFunc;

add = (n1: number, nu2: number) => {
  return n1 + n2;
};
interface AddFunc {
  (a: number, b: number): number;
}

let add: AddFunc;

add = (n1: number, nu2: number) => {
  return n1 + n2;
};

선택적 매개변수 & 변수

interface Named {
	readonly name: string;
	outputName? : string;
}

interface Greetable extends Named{
	greet(phrase: string): void;
}

class Person implements Greetable{
	name: string;
	age: 30;

	construcor(n: string){
		this.name = n;
	}
	greet(phrase: string)=>{
		console.log("phrase" : this.name)
	}),
}

user1 = new Person("Lee");
user1.name = "Kim" // Error

변수: 타입 구조를 변수?: 타입를 사용하여 해당 변수가 있을 수 도있지만 반드시 그렇지 않도록 지정할 수 있다.

만약 해당 변수를 사용할때 해당 타입으로 사용하지 않을 경우 Error가 나게 된다.



5. 고급 타입

인터섹션 타입

인터섹션 타입으로 다른 타입을 결합할 수 있다.

type Admin = {
  name: string;
  privileges: string;
};

type Employee = {
  name: string;
  startDate: Date;
};

type ElevatedEmployee = Admin & Employee;

const e1: ElevatedEmployee = {
  name: "Lee",
  privileges: ["create-server"],
  startDate: new Date(),
};

인터페이스로 표기하면 다음과 같다

interface Admin {
  name: string;
  privileges: string;
}

interface Employee {
  name: string;
  startDate: Date;
}

interface ElevatedEmployee extends Admin, Employee {}

const e1: ElevatedEmployee = {
  name: "Lee",
  privileges: ["create-server"],
  startDate: new Date(),
};

두 유니언 타입이 교차하는 유니언 타입이 된다.

type Combinable = string | number;
type Numberic = number | boolean;

type Universal = Combinable & Numberic;

유니언 타입은 타입간에 공통점이 있는 타입으로 Combinable 타입과 Numberic 타입은 각각 number 타입을 가지고 있다

타입 가드

위의 Combinable 타입은 유니언 타입으로 문자열이나 숫자형을 지니는데 타입가드는 유니언 타입을 돕는다.

타입의 유연성을 가지면 좋지만, 런타임 시 정확히 어떤 타입을 얻게 될지 알아야 하는 경우가 많기 때문이다.

type Combinable = string | number;
type Numberic = number | boolean;

type Universal = Combinable & Numberic;

function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    // 타입가드
    return a.toString() + b.toString();
  }
  return a + b;
}

a와 b가 숫자형이어야 a+b를 하고 둘중 하나라도 숫자가 아니면 if문이 실행한다.

이 라인을 타입 가드라고 하고, 이는 유니온 타입이 지닌 유연성을 활용하게 해주며 런타임시 코드가 정확하게 작동하게 해준다.

아래와 같은 상황에서 타입 가드가 필요하다.

아래는 UnknowEmplyee가 Employee 또는 Admin 타입을 지닌다.

그럴때 function에서 정확한 타입을 알수 있도록 해야하기 때문에 필요하다.

type Admin = {
  name: string;
  privileges: string;
};

type Employee = {
  name: string;
  startDate: Date;
};

type UnknowEmplyee = Employee | Admin; // Employee 또는 Admin 타입을 지닌다.

function printEmployeeInformation(emp: UnknowEmplyee) {
  console.log("Name : ", emp.name);
  console.log("privileges : ", emp.privileges); // Error
}

위와 같이 Error가 날때 typeOf를 if문으로 확인할 수 없기 때문에 아래와 같이 사용한다.

type Admin = {
  name: string;
  privileges: string;
};

type Employee = {
  name: string;
  startDate: Date;
};

type UnknowEmplyee = Employee | Admin; // Employee 또는 Admin 타입을 지닌다.

function printEmployeeInformation(emp: UnknowEmplyee) {
  if ("privileges" in emp) {
    console.log("Name : ", emp.name);
    console.log("privileges : ", emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Name : ", emp.name);
    console.log("startDate : ", emp.startDate);
  }
}

printEmployeeInformation({ name: "Lee", startDate: new Date() });

JS의 in을 사용하면 privilegesemployee에 속성으로 존재하는지를 알 수 있다.

클래스 타입 가드

클래스에서는 instanceof 타입가드로 사용할 수 있다.

우선 다음과 같이 위에서 했던 방식으로 타입가드를 적용할 수 잇다.

class Car {
  drive() {
    console.log("Driving...");
  }
}

class Truck {
  drive() {
    console.log("Driving...");
  }
  loadCargo(amount: number) {
    console.log("Driving..." + amount);
  }
}

type Vehicle = Car | Truck;

const v1 = new Car();
const v1 = new Truck();

function useVehicle(vehicle: Vehicle) {
  vehicle.drive();
  if ("loadCargo" in vehicle) {
    vehicle.loadCargo(100);
  }
}

useVehicle(v1);
useVehicle(v2);

instanceof 사용

function useVehicle(vehicle: Vehicle) {
  vehicle.drive();
  if ("loadCargo" instanceof vehicle) {
    vehicle.loadCargo(100);
  }
}

useVehicle(v1);
useVehicle(v2);

구별된 유니언

타입 가드를 쉽게 구별할 수 있게 해주는 유니언 타입으로 작업을 수행할 때 사용할 수 있는 패턴으로 객체 타입으로 작업할 때도 사용할 수 있다.

interface Bird {
  flyingSpeed: number;
}

interface Horse {
  runningSpeed: number;
}

type Animal = Bird | Horse;

function moveAnimal(animal: Animal) {
  if ("flyingSpped" in animal) {
    console.log("Moving with speed: ", animal.flyingSpeed);
  }
}

위에서 했던 것처럼 in을 사용하여 할 수 있지만, 동물들이 더 많아질수록 즉, 입력값이 많아질 수록 확인해야할 사항이 많아진다.

인터페이스로 구현하는 중이니 intanceof는 사용할 수 없다.

이때 유니언과 추가 속성의 일부가 되어야 하는 객체를 인터페이스마다 입력함으로써 구별된 유니언을 구축할 수 있다. 아래와 같이 type이라는 리터럴 타입을 하나 만들어준다.

interface Bird {
  type: "bird";
  flyingSpeed: number;
}

interface Horse {
  type: "horse";
  runningSpeed: number;
}

type Animal = Bird | Horse;

function moveAnimal(animal: Animal) {
  let speed;
  switch (animal.type) {
    case "bird":
      speed = animal.flyingSpeed;
      break;
    case "bhorseird":
      speed = animal.runningSpeed;
  }
  console.log("Moving with speed: ", speed);
}

moveAnimal({ type: "bird", runningSpeed: 10 });
moveAnimal({ type: "Horse", runningSpeed: 10 });

형 변환(typecating)

형 변환은 타입스크립트가 직접 감지하지 못하는 특정 타입의 값을 타입스크립트에 알려주는 역할을 한다.

const paragraph = document.querySelector("p");
const paragraph = document.queryElementById("message-id"); // HTMLElement | null

userInuputElement.value = "Hi there!";

위와 같을때 null이 아닌 HTMLElement를 알려야 할때 사용할 수 있다.

리액트와 같이 JSX 코드에서 사용할때 JSX구문과의 충돌을 맏기 위해 홀화살괄호(<>) 형 변환에 대안을 제공한다.

const paragraph = document.querySelector("p");
const paragraph = document.queryElementById("message-id")! as HTMLInputElemnet;

userInuputElement.value = "Hi there!";

느낌표 !를 사용하여 앞의 표현식을 null로 반환하지 않겠다고 TS에게 인식 시킬 수 있다.

위에 처럼했는데 null를 반환하지 않을 것이라는 확신이 들지 않은 경우 다음과 같이 if문을 사용할 수 있다.

const paragraph = document.queryElementById("message-id");
if (userInputElment) {
  (userInputElement as HTMLInputElement).value = "Hi there!";
}

인덱스 속성

객체가 지닐수 있는 속성에 대해 보다 유연한 객체를 생성하게 해주는 기능이다.

예를 들어 이메일 필드에서 이메일인지 여부를 확인하고 이메일이 아니라면 적합한 에러 메시지를 에러 컨테이너에 추가해야한다.

interface ErroContainer {
  // { email: "Not a valid email", username : "Muststart with a charcter!" }
}

이는 문자열이어야하지만 몇개의 속성을 가질지, 어떤 속성이 어떤 이름을 가질지 미리 알수 업삳.

이때 인덱스 타입을 사용할 수 있다.

interface ErroContainer {
  // { email: "Not a valid email", username : "Muststart with a charcter!" }
  id: number; // X
  bool: boolean; // X
  [prop: string]: string;
}

인덱스 타입에는 number나 boolean을 설정할 수 없다.

interface ErroContainer { // { email: "Not a valid email", username : "Muststart with a charcter!" }
	[prop: string]: string;
}

const errorBag: ErrorContainer{
	email: "Not a valid email!",
	username: 'Muststart with a charcter!'
}

함수 오버로드

동일함 함수에 대해 여러 함수 시그니처를 정의할 수 있는 기능으로, 간단하게 말해 다양한 매개변수를 지닌 함수를 호출하는 여러 가지 가능한 방법을 사용하여 함수 내에서 작업을 할 수 있게 한다.

type Combinable = string | number;
type Numberic = number | boolean;

type Universal = Combinable & Numberic;

function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    // 타입가드
    return a.toString() + b.toString();
  }
  return a + b;
}

const result = add("Lee", "Kim"); // X
result.split(""); // X

const result = add("Lee", "Kim") as string; // O

함수 오버로드는 함수 위에 같은 이름을 사용하면 된다.

이 오버로드를 사용하면 매개 변수를 더 많이 또는 더 적게 사용 가능하다.

type Combinable = string | number;
type Numberic = number | boolean;

type Universal = Combinable & Numberic;

// function add(n: number): number
function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    // 타입가드
    return a.toString() + b.toString();
  }
  return a + b;
}

const result = add("Lee", "Kim"); // X
result.split(""); // X

const result = add("Lee", "Kim") as string; // O

이럴 경우 매개변수가 string일때는 string을 반환을, number일때는 number를 항상 반환한다.

선택적 체이닝 (opsinal)

예시로 백엔드에서 데이터를 가져오고 DB나 객체내 특정 속성이 정의되어 있는지 확실하지 않는 소스에서 데이터를 가져오는 애플리케이션이 있다고 가정한다.

const fetchedUserDate = {
  id: "one",
  name: "Lee",
  job: { title: "CEO", description: "My own Compoany" },
};

console.log(fetchedUserDate.job.title);

물론 다음과 같을때는 실행이 되지만 이 데이터를 가져오면서 필요한 데이터를 모두 가져오지 못할 겨웅가 있다.

객체의 일부 속성이 설정되어 있는지 또는 정의되지 않았는지 확실히 알 수 없는 중첩된 데이터로 구조화 작업을 수행하는 경우고 있다.

따라서 job이 존재하고 어떤 이유로 데이터를 가져오지 못한다면 에러가 발생한다.

const fetchedUserDate = {
  id: "one",
  name: "Lee",
  // job : { title: "CEO", description: "My own Compoany" }
};

console.log(fetchedUserDate?.job?.title);

Null 병합

null 데이터 처리에 도움을 주는 null 병합이다.

어떤 데이터나 입력값이 있는데 그것이 Null인지, undefined인지, 유효한 데이터인지 알 수 없을때, 예시로 userInputnull이면 이를 하드코딩해서 null인지를 알 수 있지만 dom api를 통해 가져오면 이에 대해 확실히 알지 못한다.

const userInput = null;

그런데 백엔드에서 가져온다면 개발자는 미리 알지만 TS에서는 null여부를 알 수 없고, 그래서 이를 다른 상수나 storedData와 같은 변수로 저장하여 null일때 폴백(fallback) 값을 저장하도록 한다.

const userInput = "";
const storedData = userInput || "DEFAULT";

console.log(storedData); // "DEFAULT"

userInput이 작업중인 데이터가 실제로 null이나 undefined가 아니면 다른 방법을 써야한다.

다음과 같이 ?? 라는 null 병합 연산자를 사용한다.

const userInput = "";
const storedData = userInput ?? "DEFAULT";

console.log(storedData);
// userInput = '' 일때 출력값 : null
// userInput = undefiend 일때 출력값 : DEFAULT

이것은 null이거나 undefined라면 즉, 빈문자열이나 0이 아닌 null이나 undefiend 둘 중 하나라면 풀백을 사용해야 한다는 의미이다.



6. 제네릭

제네릭이란?

제네릭 타입은 클래스를 인스턴스화하면서 구체적인 타입을 설정하지만 구체적인 타입을 생성하면서 클래스나 함수를 해당 타입으로 제한하기 보다 유연하게 어느정도의 제약 조건만 지정할 수 있다.

그리고 이 제약조건은 선택적이며, 제약조건이 없거나, 많은 제약 조건을 지정할 수 있다.

const names: Array = []; 와 같이 하면 Array<T> 라는 에러가 나온다.

이것은 제네릭 타입이다.

제네릭 타입은 다른 타입과 연결되는 종류인데 다른 타입이 어떤 타입이어야 하는지에 대해서는 크게 상관하지 않는다.

배열 예시로 되돌아 가서

const names: Array<string | number> = [];
const names: Array<any> = [];

와 같이 입력할 수 있고 타입에 대한 정보를 입력하지 않으려는 경우 any를 사용할 수 있다.


// error
const names: Array = [];
names[0].split(’’);

// true
const names: Array<string> = [];
names[0].split(’’);

위와 같이할 때에 string 타입임을 알 경우 에러가 나지 않는다.

제네릭 함수

function merge(objA: object, objB: object) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, { age: 30 });

mergedObj.name; // Error
mergedObj.age; // Error

위와 같이 객체를 합치는 함수를 사용했을대 mergedObj는 결과값에 대한 값을 접근할 수 없다.

타입스크립트가 객체를 반환한다는 것을 추론하지만 결과값을 모르기 때문이다.

물론 다음과 같이 형 변환을 쓸 수는 있지만 번거로워 진다.

function merge(objA: object, objB: object) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, { age: 30 }) as {
  name: string;
  age: number;
};

mergedObj.name; // Error
mergedObj.age; // Error

일일이 값을 쓰기에는 번거롭기 때문에 이때 제네릭을 쓰게 된다.

<T>를 사용하는데 이는 다른 식별자를 사용해도 되지만 타입의 첫글자인 T를 사용하는게 일반적인 관례이다. 그리고 함수 내에서 사용하는 두번째 제네릭 매개변수나 타입의 이름은 <U>로 지정한다.

function merge<T, U>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, { age: 30 });

mergedObj.name; // "Lee"
mergedObj.age; // 30

이렇게 하면 이 merge함수에 어떤 값을 넣든지 상관할 필요가 없게 됩니다.

만약 name,age를 각각 type으로 지정하게 될 경우 이후 다른 객체를 넣게 대면 에러가 나오게 됩니다.

function merge<T, U>(objA: { name: string }, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, { age: 30 });
const mergedObj = merge({ hobbies: "game" }, { age: 30 }); // error

이러한 타입을 고정적으로 설정하지 않고 함수를 호출할때 동적으로 설정 되었다는 것을 알 수 있습니다.

제약 조건

만약 다음과 같이 숫자만 입력할대 문제가 생긴다.

function merge<T, U>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, 30);

mergedObj.name; // "Lee"

하지만 name 객체는 그대로 새용 가능하다.

이때는 다른 에러나 문제가 표시 되지 않고, 30이 병합 되지도 않는다.

그래서 타입 제약 조건을 사용해서 구현할 수 있다.

function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: "Lee" }, 30); // Error

이렇게 되면 T타입이 어떤 구조를 가지든 상관 없지만 일단은 객체여야 한다는 의미가 되게 된다.

<T extends object>에는 <T extends Person>과 같이 직접 만든 타입이나 <T extends string | number>와 같이 유니언 타입도 가능하다.

일반 함수

function countAndPrint<T>(element: T) {
  let descriptionText = "Got no value";
  if (element.length > 0) {
    descriptionText = "Got 1 elements";
  } else if (element.length > 1) {
    descriptionText = "Got" + element.length + "elements";
  }
  return [element, descriptionText];
}

이 함수에서 length에 에러 표시(불명확하다고)가 나온다.

그래서 이를 구체적으로 명시해야 한다.

다음과 같이 제약조건을 사용한다.

interface Lengthy {
  length: number;
}

function countAndPrint<T extends Lengthy>(element: T): [T, string] {
  let descriptionText = "Got no value";
  if (element.length > 0) {
    descriptionText = "Got 1 elements";
  } else if (element.length > 1) {
    descriptionText = "Got" + element.length + "elements";
  }
  return [element, descriptionText];
}
keyOf” 제약 조건
function extractAndConvert(obj: obejct, key: string) {
  return "Value" + obj[key]; // Error
}

function extractAndConvert<T extends object, U extends keyof T>(
  obj: T,
  key: U
) {
  return "Value" + obj[key];
}

extractAndConvert({ name: "Lee" }, "name");
extractAndConvert({ name: "Lee" }, "age"); //Error

반환 값 지정으로 에러 방지

class 제네릭

class DataStorage {
  private data = [];

  addItem(tiem) {
    this.data.push(item);
  }

  removeItem(item) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

item 타입을 입력하지 않았기 때문에 Error가 발생한다.

이때 제네릭 클래스로 바꾸어서 데이터의 타입이 무엇이든 상관없게 한다.

class DataStorage<T> {
  private data: T[];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("Hello");
textStorage.addItem("world");
textStorage.removeItem("Hello");

consoel.log(textStorage.getItems()); // "world"

텍스트를 저장하지 않아야 할 수 도 있고, 숫자를 입력해야 할수도 있다.

숫자가 필요할때는 숫자로

const textStorage = new DataStorage<number>();

유니언 타입으로도

const textStorage = new DataStorage<string | number>();

이때 생기는 문제로는 객체나 배열로 DataStorage를 만들었을때 removeItem를 실행하면 삭제가 되지 않는다.

const storage = new DataStorage<obejct | array>();

storage.addItem({ name: "Lee" });
storage.addItem({ hobby: "movie" });
storage.removeItem({ name: "Lee" });
console.log(storage.getItems); // { name: "Lee" }

JS에서의 객체는 참조 타입이기 때문이다.

만약 JS에서는 배열에서 마지막 요소를 제거하게 되지만 만약 아무것도 찾지 못하면 indexof가 -1를 반환하게 되고 제거를 하지 않게 된다.

다음과같이 item을 제거하지 않도록 수정할 수 있다.

...
removeItem(item: T){
	if(this.data.indexOf(item) === -1){
		return;
	}
	this.data.splice(this.data.indexOf(item), 1);
}
...

하지만 객체를 함께 작동할 수 있도록 하는 유일한 방법은 정확히 같은 객체를 다시 전달하도록 하는 것이다.

let nameObj = { name: "Lee" };

storage.addItem(nameObj);
storage.addItem({ hobby: "movie" });
storage.removeItem(nameObj);
console.log(storage.getItems); // { name: "Lee" }

그래서 (object나 array가 아닌) 원시 값만하고만 작동하게 수정할 수 있다.

class DataStorage<T extends string | number | boolean> {
  private data: T[];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

제네릭 유틸리티 타입

파셜(partial) 타입

interface CourseGoal {
  title: string;
  description: stirng;
  complteUntil: Date;
}

function createCourseGoal(
  title: stirng,
  description: stirng,
  date: Date
): CourseGoal {
  return { title: stirng, description: stirng, date: Date };
}

function createCourseGoal(
  title: stirng,
  description: stirng,
  date: Date
): CourseGoal {
  let courseGoal: courseGoal = {};
  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.complteUntil = complteUntil;

  return courseGoal;
}

파셜 타입을 사용하여 모든 속성이 선택적인 객체 타입으로 바꿀 수 있다.

interface CourseGoal {
  title: string;
  description: stirng;
  complteUntil: Date;
}

function createCourseGoal(
  title: stirng,
  description: stirng,
  date: Date
): CourseGoal {
  let courseGoal: partial<CourseGoal> = {};
  courseGoal.title = title;
  courseGoal.description = description;
  courseGoal.complteUntil = complteUntil;

  return courseGoal as CourseGoal;
}

Readonly 타입

const names = ["Lee", "Kim"];
names.push("Choi");

위의 names 배열을 잠궈서 더이상 추가 할 수 없게 한다.

const names: Readonly<string[]> = ["Lee", "Kim"];
names.push("Choi"); // Error[]
names.pop();

제네릭 타입 vs 유니언 타입

// 유니언 타입
class DataStorage {
  private data: (string | number | boolean)[];

  addItem(item: string | number | boolean) {
    this.data.push(item);
  }

  removeItem(item: string | number | boolean) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}
const textStorage = new DataStorage<string>(); // Error

// 제네릭 타입
class DataStorage<T extends string | number | boolean> {
  private data: T[];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    if (this.data.indexOf(item) === -1) {
      return;
    }
    this.data.splice(this.data.indexOf(item), 1);
  }

  getItems() {
    return [...this.data];
  }
}

위의 유니언 타입은 문자열, 숫자, 불리언 같은 모든 유형의 데이터를 추가하는 것이기 때문에

const textStorage = new DataStorage<string>();를 사용하게 되면 에러가 나온다.

const textStorage = new DataStorage();으로 타입을 지우면 작동한다.

제네릭에서는 처음 지정할때 string으로 할 경우 값을 추가할때 string이 아니면 error가 나지만 유니언 타입으로 지정하면 stirng이 아닌 numberboolean 값을 추가 할 수 있게 된다.

유니언 타입은 모든 메서드 호출이나, 모든 함수 호출에서 다른 타입을 지정할 경우에는 유용하지만 그렇지 않고 한 타입으로 고정시켜두려면 제네릭 타입을 사용한다.



6. 데코레이터

데코레이터는 meta programming하는데에 유용하게 사용할 수 있다.

데코레이터

메타 프로그래밍은 end user가 페이지를 방문하는데 보통 곧바로 영향을 주기에 데코레이터를 사용하지 않는다.

대신에 코드를 쓰는데 적합하도록 만들어서 데코레이터를 다른 개발자들이 사용하기 쉽게 한다.

설정

class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

const pers = new Person();

console.log(pers);

데코레이터로 변환.

첫 글자는 보통 대문자로 시작한다.

function Logger(constructor: Function) {
  console.log("Logging...");
  console.log(constructor);
}

@Logger()
class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

const pers = new Person();

console.log(pers);

데코레이터 팩토리 작업

위와 같이 데코레이터를 만드는 대신 데코레이터 팩토리(factory)를 정의할 수 있다.

function Logger(LogString: string) {
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

@Logger("LOGGING - PERSIN")
class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

const pers = new Person();

console.log(pers);

유용한 데코레이터

템플릿과 함께 새 데코레이터 팩토리를 만들 수 있다.

function WithTemplate(template: string, hookId: strubg) {
  return function (_: Function) {
    // _는 존재는 알지만 쓰지 않을때 명시
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

// @Logger("LOGGING - PERSIN")
@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

const pers = new Person();

console.log(pers);

다음과 같이 html tag에 id에 hookEl에 다음 태그를 삽입할 수있다.

이를 컴포넌트처럼 사용할 수 있다.

여러 데코레이터 추가

위이 데코레이터에서 logger를 살리면 아래가 먼저 실행되고 위에 있는 데코레이터가 나중에 실행 되는 것을 알 수 있다.

function Logger(LogString: string) {
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: strubg) {
  return function (_: Function) {
    // _는 존재는 알지만 쓰지 않을때 명시
    const hookEl = document.getElementById(hookId);
    if (hookEl) {
      hookEl.innerHTML = template;
    }
  };
}

@Logger("LOGGING - PERSIN") // 2번째 실행
@WithTemplate("<h1>My Person Object</h1>", "app") // 1번째 실행
class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

const pers = new Person();

console.log(pers);

속성 데코레이터

데코레이터의 실행은 클래스의 정의 될때 실행된다.

(컨스트럭트 함수의 일부로)

function Log(target: any, propertyName: string | number)  {
	console.log("Property decorator!");
	console.log(target); // 오브젝트의 프로토타입 (title, price값은 보이진 않음)
	console.log(propertyName); // "title"   // 프로퍼티 이름
}

function Log2(target: any, name: string, descriptor: PropertyDescriptor) {
	console.log("Accessor decorator!");
	console.log(target);
	console.log(name); // price
	console.log(descriptor); // { get: undefined, set : ... }
}

function Log3(target: any, name: string | Symbol, descriptor: PropertyDescriptor){
		console.log("Method decorator!");
		console.log(target);
		console.log(name); // getPriceWithTax
		console.log(descriptor); // {}
}

function Log4(target: any, name: string | Symbol, position: number){
	console.log('Parameter decorator!');
	console.log(target);
	console.log(name); // getPriceWithTax
	console.log(position); // 0 (인수 tax의 index 즉 0으로 시작해서 숫자 0)
}

class Product {
	@Log
	title: string;
	private	_price: number;

	@Log2
	set Price(val: number){
		if(val > 0){
			this._price = val;
		}else{
			thor new Error("Invalid price - should be positive!");
		}
	}

	constructor(t: string, p: number){
		this.title = t;
		this.price = pl
	}

	@Log3
	getPriceWithTax(@Log4 tax: number){
		return this._price * (1 + tax);
	}
}

데코레이터는 언제 실행하는가?

클래스를 정의할때

const p1 = new Product("Book", 19);
const p2 = new Product("Book", 19);

클래스 데코레이션에서 클래스 반환 및 변경

데코레이터의 반환 값을 이용해서 데코레이터에 추가한 클래스를 전혀 다른 클래스로 대체한다.

// 데코레이터 팩토리
function WithTemplate(template: string, hookId: string) {
  console.log("Template Factory");

  return function <T extends { new (...args: any[]): { name: string } }>(
    originalConstructor: any
  ) {
    return class extends originalConstructor {
      constructor(..._: any[]) {
        super();
        // 클래스가 정의될때 돌아가지 않는 로직 추가할 수 있다.
        console.log("Rendering template");
        const hookEl = document.getElementById(hookId);
        if (hookEl) {
          hookEl.innerHTML = template;
          hookEl.querySelector("h1")!.textContent = this.name;
        }
      }
    };
  };
}

@WithTemplate("<h1>My Person Object</h1>", "app")
class Person {
  name = "Lee";

  constructor() {
    console.log("Person Object");
  }
}

유효성 검사용 데코레이터

class Course {
  title: stirng;
  price: number;

  consturcotr(t: string, p: number) {
    this.title = t;
    this.price = p;
  }
}
const courseForm = document.querySelector("Form")!;
courseForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const titleEl = document.getElemnetById("title") as HTMLInputElemnet;
  const priceEl = document.getElemnetById("price") as HTMLInputElemnet;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);
  console.log(createdCourse);
});

→ 정상적으로 출력된다. → 하지만 아무 값을 입력하지 않았을때도 추가된다. → 중간에 if문으로 추가할 수 있다.

하지만 새로운 코스를 생성할때마다 추가하기전에 유효성 로직을 추가해야되게 된다.

interface ValidatorConfig {
  [property: string]: {
    [validatableProp: string]: string[]; 
  };
}

const registeredValidators: ValidatorConfig = {};

function Required(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: ["required"],
  };
}

function PositiveNumber(target: any, propName: string) {
  registeredValidators[target.constructor.name] = {
    ...registeredValidators[target.constructor.name],
    [propName]: ["positive"],
  };
}

function validate(obj: any) {
  const objValidatorConfig = registeredValidators[obj.constructor.name];
  if (!objValidatorConfig) {
    return true;
  }
  let isValid = true;
  for (const prop in objValidatorConfig) {
    for (const validator of objValidatorConfig[prop]) {
      switch (validator) {
        case "required":
          isValid = isValid && !!obj[prop];
          break;
        case "positive":
          isValid = isValid && obj[prop] > 0;
          break;
      }
    }
  }
  return isValid;
}

class Course {
  @Required
  title: string;
  @PositiveNumber
  price: number;

  constructor(t: string, p: number) {
    this.title = t;
    this.price = p;
  }
}

const courseForm = document.querySelector("Form")!;
courseForm.addEventListener("submit", (event) => {
  event.preventDefault();
  const titleEl = document.getElemnetById("title") as HTMLInputElemnet;
  const priceEl = document.getElemnetById("price") as HTMLInputElemnet;

  const title = titleEl.value;
  const price = +priceEl.value;

  const createdCourse = new Course(title, price);

  if (!validate(createdCourse)) {
    alert("Invalid input, please try again!");
    return;
  }
  console.log(createdCourse);
});