Generics, Any와 다른 점
function helloString(message: string): string {
return message
}
function helloNumber(message: number): number {
return message
}
- 어떤 함수가 들어오는 인자와 나가는 return 타입이 일정한 규칙을 이루면서 같은 로직을 반복하는 함수 → 더 많이 반복되는 함수들이 생긴다.
- 해당 문제 해결을 위해서, any 사용 (모든 타입을 받고 리턴할 수 있기 때문이다.) 그러나 우리의 의도와는 다르게 다른 결과를 가져오게 된다.
Any 사용
function hello(message: any): any {
return message
}
console.log(hello("Mark").length);
console.log(hello(19).length);
- 문자열에서 사용하는 length를 사용하게 되면 any 형식으로 결과가 나오게 된다.
- 심지어, length를 사용할 수 없는 number 타입도 사용할 수 있게 된다. any 타입이 되기 때문이다. 대신 런타임에 결과는 undefined이다.
- 이때 “Mark”와 19같이 들어가는 타입을 변수 같은 것으로 활용해서 리턴되는 되는 타입과 연관을 시켜주면 좋겠다는 아이디어에서 나온 것이 Generic이다.
Generic
function helloGeneric<T>(message: T): T {
return message
}
console.log(helloGeneric("Mark")); // "Mark"
console.log(helloGeneric("Mark").length); // number로 인식. String.length
console.log(helloGeneric(19)); // 19
console.log(helloGeneric(19).length); // number로 인식했기 때문에 length 사용 불가
console.log(helloGeneric(true)); // true
- 괄호를 사용해서 해당 괄호 안에 T라는 단어를 사용한다. 이때, T는 Type을 의미한다.
- 추가적인 변수(?)를 더 넣고 싶다면
<T, U, K>
이런 식으로 추가해주면 된다.
- message가 받은 인자를 T로 한다.
- 인자로 “Mark”를 넣으면 string으로 인식, 19를 넣으면 number로 인식한다.
- 문자열을 리터럴로 작성 ⇒ T가 문자열이 아닌 리터럴 타입으로 인식. 즉, 문자열 리터럴 타입으로 인식되었다.
- 19를 넣으면 number로 인식.
string에서만 사용 가능한 length를 사용하려면 사용이 불가능 하다고 나온다.
- true를 넣으면 true로 결과물이 나온다. true 타입으로 인식했다.
- 따라서 T를 변수처럼 사용할 수 있게 된다.
Generics Basic
generic 사용방법
function helloBasic<T>(message: T): T {
return message;
}
-
타입 지정
괄호안에 원하는 타입을 지정해서 사용한다.
해당 타입이 아닌 값을 넣게 되면 에러가 발생한다.
helloBasic<string>('number'); // string
helloBasic<string>(19); // error
-
타입 미지정
타입을 지정하지 않기 위해서 괄호를 사용하지 않으며, 값을 넣는다.
T에 들어간 값에 따라서 타입을 추론하여 결과를 도출한다.
따라서 T의 값을 number가 아닌 36으로 추론한다.
타입스크립트 입장에서 가장 좁은 범위 내에서 타입을 추론해야 하기 때문에 number가 아닌 36 그 자체로 결과를 나타낸다.
number(더 넓은 범위) > 36(numebr 안에 속함)
-
generic 변수 한개 이상 사용
function helloBasic<T, U>(message: T, comment: U): T {
return message;
}
helloBasic<string, number>('number', 19); // string
// helloBasic<string>(19); // error
helloBasic(36, 19); // 36
Generics Array & Tuple
Array
function helloArray<T>(message: T[]): T {
return message[0];
}
helloArray(['Hello','Array']); // "Hello"
helloArray(['Hello', 5]); // string이거나 number?
- T뒤에 배열로 받겠다는 표시로 대괄호를 붙여준다.
- 문자열 두개를 넣었을 때, return message[0]이기 때문에 “Hello” return
- 문자열과 숫자를 넣었을 때, string과 number가 들어갔기 때문에 결과는 “Hello”이겠지만, 추론 과정에서는 union으로 string이거나 number로 추론한다.
Tuple
function helloTuple<T, K>(message: [T, K]): T {
return message[0];
}
helloTuple(['Hello','Array']); // string
helloTuple(['Hello',5]); // 0번째 index는 string
- message를 튜플 형태의 타입으로 처리한다.
따라서 0번째 요소를 return한다면 T가 return된다.
- 문자열 두개를 넣었을 때는 string으로 return
- 문자열과 숫자를 넣었을 때, 0번째는 string, 1번째는 number로 지정되면서 0번째에대한 return값은 string이라고 나오게된다. 만약 1번째 return값을 물어봤다면 number로 나올 것이다.
Generics Function
함수의 타입 선언 방식
함수의 타입만 정의하고 정의에 generic만 추가한다면 type alias, interface 방식으로 정의하고 사용하면 된다.
기존 함수에 generic 부분만 추가하면 되는 것이기에 간편하다.
-
type alias
type HelloFunctionGeneric1 = <T>(message: T) => T;
const helloFunction1: HelloFunctionGeneric1 = <T>(message: T) => {
return message;
};
-
interface
interface HelloFunctionGeneric2 {
<T>(message: T): T;
}
const helloFunction2: HelloFunctionGeneric2 = <T>(message: T) => {
return message;
};
Singletons
singleton pattern
어플리케이션이 실행되는 중간에 클래스로 부터 단 하나의 오브젝트만을 생성해서 사용하는 패턴
static과 private 사용하기. static을 통해서 데이터를 공유.
class ClassName {
private static instance: ClassName | null = null;
public static getInstance(): ClassName {
// ClassName으로부터 만든 object가 있으면, 리턴
// ClassName으로부터 만든 object가 없으면, 생성
if (ClassName.instance === null) {
ClassName.instance = new ClassName();
}
return ClassName.instance;
}
private constructor() {}
}
const aa = ClassName.getInstance(); // 최초로 불리기에 object 생성
const bb = ClassName.getInstance(); // 생성이 되어 있기에 그냥 리턴
console.log(aa === bb); // true
상속 (Inheritance)
-
부모 클래스
class Parent {
constructor(protected _name: string, protected _age: number) {}
public print(): void {
console.log(`이름은 ${this._name}이고 나이는 ${this._age}입니다.` )
}
}
const p = new Parent("Jin", 24)
p.print()
-
자식 클래스
-
Parent 상속받은 Child 클래스가 아무것도 하지 않아도 Parent의 생성자를 그대로 가져오기 때문에 Parent와 동일한 형태로 넣어서 사용할 수 있다.
class Child extends Parent {}
const c = new Child("Son", 13)
c.print()
-
property 추가하기
접근 제어자까지 오버라이드 된다.
class Child extends Parent {
public _name = "Mark";
public gender = "male";
}
const c = new Child("Son", 13)
c._name
-
Parent의 생성자 오버라이드
자식의 생성자에서는 무조건 super가 가장 먼저 호출되어어야 한다.
부모와 자식 클래스는 각자의 영역에서 서로에게 영향을 주지 않도록 하는 것이 중요하다.
class Parent {
constructor(protected _name: string, private _age: number) {}
public print(): void {
console.log(`이름은 ${this._name}이고 나이는 ${this._age}입니다.` )
}
protected printName(): void {
console.log(this._name, this._age)
}
}
class Child extends Parent {
public gender = "male";
constructor(age: number) {
super("Mark", age);
this.printName();
}
}
const c = new Child(5);
c.print();
// Mark 5
// 이름은 Mark이고 나이는 5입니다.
Abstract Classes
Abstract를 사용하면 완전하지 않은 클래스를 표현할 수 있다.
완전하지 않은 클래스는 new 키워드를 이용하여 개체를 만들 수 없다.
완전하지 않은 개체를 상속과 같은 기능으로 완전하게 만든다.
Getters & Setters
class Person {
public constructor(private _name: string, public age: number) {}
get name() {
return this._name + " Lee";
}
set name(n: string) {
this._name = n;
}
}
const p1 = new Person("mark",39);
console.log(p1.name) // get을 하는 함수 getter
p1.name = "Jin"; // set을 하는 함수 setter
console.log(p1.name)
// mark Lee
// Jin Lee
- 말 그대로 get 함수는 얻는 행위(세팅 한 걸 출력) set 함수는 세팅을 한다.
- get에는 return이 포함되어야 한다.
- getter 혹은 setter 하나를 삭제하면 컴파일 error가 발생한다.
readonly properties
class Person {
public readonly name: string = "Mark";
private readonly country: string;
public constructor(private _name: string, public age: number) {
this.country = "Korea";
}
hello() {
this.country = "China";
// readolny로 인해 값 변경 불가
}
}
const p1 = new Person("Mark",39);
console.log(p1.name)
p1.name = "Jin"; // readonly로 인해 값 변경 불가
console.log(p1.name)
- readonly 키워드가 달려있으면 private던 public이던 초기화되는 영역에서만 값을 세팅할 수 있다. 다른 곳에서는 값을 변경할 수 없다.
- 초기화하는 부분과 constructor 안에서만 readonly가 달려있는 private 프로퍼티의 값을 지정해 줄 수 있다. 그 외의 다른 메서드에서는 값을 변경할 수 없다.
Index Signatures in class
클래스 안에서 인덱스 시그니처 사용하기
index signature은 프로퍼티가 고정된 것이 아닌 동적으로 프로퍼티의 이름이 바뀔 때 사용하기 유용하다.
// 해당 형태로 만들기
// class => object
// {mark: "male", jade: "male"}
// {chloe: "female", alex: "male", anna: "female"}
class Students {
[index: string]: string;
// [index: string]: "male" | "female";
// 위 코드가 조금 더 정확하다. 두 가지 값만 올 수 있도록 하기 때문이다.
mark: "male" = "male"; // mark가 모든 반에 항상 존재할 때
}
const a = new Students;
a.mark = 'male';
a.jade = 'male';
console.log(a);
// {mark: "male", jade: "male"}
- 프로퍼티의 이름을 직접 쓰게되면 새로운 값을 넣게 되었을 때 또 직접 적어줘야 한다. 동적으로 처리가 불가능해진다.
- 이런 경우, 프로퍼티가 동적으로 사용하게 하려면 index signature를 사용한다.
- index signature은 초기값을 설정하지 않는다.
값이 있을 수도 있고 없을 수도 있기 때문이다. 그래서 따로 최초값을 정하지 않는다.
- mark라는 친구가 a,b에 항상 존재하고 male 값이 일때는 class 안에
mark: “male” = “male”;
형식으로 설정하면 된다.
나머지는 optional하게 등록이 가능하다.
Static Properties & Methods
프로퍼티와 메소드 앞에 static 키워드 사용하기
-
static method
class Person3 {
public static hello() {
console.log("hello~?");
}
}
const p3 = new Person3();
// p3.hello();
Person3.hello();
- static을 붙이게 되면 object에서는 해당 함수를 메소드라고 생각하지 않는다.
따라서 p3.hello();
로는 사용할 수 없다.
Person3.hellog();
로 사용이 가능하다.
-
static property
class Person3 {
private static CITY = 'Seoul';
public static hello() {
console.log("hello~?", Person3.CITY);
}
}
const p3 = new Person3();
// p3.hello();
Person3.hello();
// Person3.CITY;
- Person3을 new로 해서 object만들지 않더라도 CITY를 공유할 수 있다.
- 클래스로 부터 만들어진 오브젝트에서 공통적으로 사용하고 싶은 데이터가 있다면 static을 넣어서 가져다 사용할 수 있도록 하면 좋다.
-
서로 값을 공유하는 형태
class Person3 {
private static CITY = 'Seoul';
public hello() {
console.log("hello~?", Person3.CITY);
}
public change() {
Person3.CITY = "Suwon";
}
}
const p3 = new Person3();
p3.hello();
const p4 = new Person3();
p4.hello();
p3.change(); // 값 변경하기
p4.hello(); // 변경된 값 다시 호출
- p3에서 CITY의 데이터를 변경
- p4에서 Person3.CITY를 꺼내면 p3에서 바뀐 데이터가 들어있다.
What are Classes
클래스 Class
- object를 만드는 blueprint (청사진, 설계도)
- 클래스 이전에 object를 만드는 기본적인 방법은 function
- JavaScript에도 class는 es6부터 사용 가능 (대신 기능이 부족)
- 타입스크립트는 OOP(객체 지향 프로그래밍)을 위한 초석
- TypeScript에서는 클래스도 사용자가 만드는 타입의 하나
Quick Start - class
- class 키워드를 이용하여 클래스를 만든다.
- class 이름은 보통 대문자를 이용한다.
- new를 이용하여 class를 통해 object를 만든다.
- constructor를 이용하여 object를 생성하면서 값을 전달한다.
constructor() → 밖에서 들어오는 데이터를 설정한다.
- this를 이용해서 만들어진 object를 가리킨다.
- JS로 컴파일 되면 es5의 경우 function으로 변경된다.
js에서는 class를 es6부터 사용이 가능하다.
es5까지는 ts파일의 class를 js로 변환하면 function으로 변환한다.
constructor & initialize
- 생성자 함수가 없으면, 디폴트 생성자가 불린다.
- 프로그래머가 만든 생성자가 하나라도 있으면, 디폴트 생성자는 사라진다.
- strict 모드에서는 프로퍼티를 선언하는 곳 또는 생성자에게 값을 할당해야 한다.
- 프로퍼티를 선언하는 곳 또는 생성자에게 값을 할당하지 않는 경우에는 !를 붙여서 위험을 표현한다.
- 클래스의 프로퍼티가 정의되어 있지만, 값을 대입하지 않으면 런타임에 undefined된다.
- 생성자에는 async를 설정할 수 없다.
new 할때는 awit 처리 할 수 없고 별도의 함수를 만들어서 해당 함수를 awit 처리하는 방식으로 값을 초기화한다. 이런 방식으로 초기화 할때는 느낌표를 붙여서 뒤늦게 초기화 된다는 것을 프로그래밍 적으로 표시해야 한다.
접근 제어자 (Access Modifiers)
- 접근 제어자는 public, private, protected가 있다.
- 설정하지 않으면 public(외부 접근 가능)이다.
- 클래스 내부의 모든 곳(생성자, 프로퍼티, 메소드)에 설정이 가능하다.
- private로 설정하면 클래스 외부에서 접근할 수 없다.
- 자바스크립트에서 private를 지원하지 않아 오랫동안 프로퍼티나 메서드 이름 앞에 _를 붙여서 표현했다. 이것은 일종의 습관으로 특별한 의미가 있는 것은 아니다.
initialization in constructor parameters
생성자 name과 age에 접근 제어자를 추가하면 this.name등의 코드를 적지 않아도 된다. (불필요한 코드 줄이기)
class Person {
public constructor(public name: string, private age: number) {
}
}
const p1 = new Person("mark",39);
console.log(p1)
function interface
인터페이스로 함수 표현하기
-
에러 발생
function의 age 부분에 물음표를 제거해서 에러가 발생했다.
interface HelloPerson {
(name: string, age?: number): void;
}
const helloPerson: HelloPerson = function (name: string, age: number){
console.log(`안녕하세요! {name} 입니다!`);
}
helloPerson function 뒤부터 적혀있는 실제로 구현한 내용보다는 (name: string, age:number 등) HelloPerson과의 관계가 더 중요하다.
그렇기에 helloPerson의 age는 number만 HelloPerson의 age는 number, undefined를 포함하기에 둘 사이 관계가 맞지 않아서 에러가 발생하게 된다.
Readonly Interface Properties
인터페이스 property의 readonly 키워드 알아보기
readonly 키워드는 추후에 값을 변경하지 않게 하고 싶을 때 사용한다.
해당 키워드를 사용하게 되면 해당 값을 변경하게 되었을 때 에러가 발생한다.
readonly를 사용하게 되면 class에서도 readonly를 받아서 표현할 수 있기에 유용하다.
type alias vs interface
- function 표현
- array 표현
-
type alias
string array([])표현을 한 다음에 이것을 어떠한 이름으로 부를 지 별칭 지정한다.
type PersonList = string[];
-
interface
indexable 타입을 사용한다.
(이전에 배운 indexable 타입은 optional하게 사용하기 위해서 number가 아닌 string으로 사용했었다.)
indexable 타입을 배열로 사용하려는 것이기에 index 뒤에 number사용 ( [0], [1] 이런식으로 표현 될 것이다.)하고 그 뒤에 string 값을 할당한다.
interface IPersonList {
[index: number]: string;
}
-
intersection 표현
interface ErrorHanding {
success: boolean;
error?: {message: string};
}
interface ArtistData {
artists: {name: string}[];
}
-
union types
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim: void;
layEggs(): void;
}
type PetType = Bird | Fish;
interface IPet extends PetType {}
class Pet implements PetType {}
-
type alias
type PetType = Bird | Fish;
-
interface
인터페이스로는 union type을 만들 수 없다.
interface IPet extends PetType {}
class Pet implements PetType {}
error! An interface cas only extends as objects type or intersection of object types with statically known members.
error! class cas only extends as objects type or intersection of object types with statically known members.
-
interface - Declaration Merging
type alias에서는 할 수 없는 기능으로 오직 interface만 가능하다.
똑같은 이름의 interface를 두 군데에서 사용하더라도 나중에 MergingInterface를 사용 할 때에는 하나로 합쳐지게 된다. 즉 말 그대로 선언이 Merging 되는 것이다.
mi.
을 하게 되면 a와 b 모두 나오게 된다.
type을 아래와 같이 선언하게 되면 동일한 이름으로 또 만들었다고 에러가 발생하게 된다.
Declaration Merging 유용하게 사용할 때
html element를 확장하게 될때, 기존에 있는 인터페이스에 추가를 하는 것이다. 이럴 때 기존에 있던 것과 새롭게 추가한 것 모두 사용을 해야 하기에 두 내용을 합쳐야 된다. 우리는 이와 같은 상황에서 유용하게 사용할 수 있다.
interface MergingInterface {
a: string;
}
interface MergingInterface {
b: string;
}
let mi: MergingInterface;
mi.