Typescript 기초 문법

Seung Hyeon ·2023년 10월 18일
1

스터디

목록 보기
1/2
post-thumbnail

※ VSCode에서 타입스크립트 실행방법

  1. npm install -g ts-node
  2. index.ts 파일 생성 후 ts-node index.ts 실행
  • 타입 스크립트는 변수나 함수 등 Type을 정의할 수 있다. → 타입 표기 사용
    • 기본 자료형 (string, boolean, number 등)
    • 참조 자료형 (object, array, function 등)
    • 추가 제공 자료형 (tupe, enum, any, void, never 등)

타입스크립트 기본


기본자료형

object와 reference형태가 아닌 실제 값을 저장하는 자료형

// string
let str: string = "hi"

// boolean
let isSucceed: boolean = true

// number
let decimal: number = 6

// null
let n: null = null

// undefined
let u: undefined = undefined

참조 자료형

객체, 배열, 함수와 같은 Object 형식의 타입

string, boolean, number, null, undefined 제외한 나머지

// 예시1
function create(o: object): void{}

create({prop: 0})  // 성공
create([1,2,3])  // 성공
create("string") // error
create(42) // error

// 예시2
let arr: number[] = [1,2,3]

// 예시3
let arr: Array<number> = [1,2,3]

추가 제공 자료형

타입스크립트에서 개발자의 편의를 위해 추가로 제공하는 타입

// tuple (길이와 각 요소의 타입이 정해진 배열을 저장하는 타입)
let arr: [string, number] = ["Hi", 6]

// enum (특정 상수들의 집합을 저장하는 타입)
enum Car {Bus, Taxi, Suv}
let bus: Car = Car.Bus
let bus: Car = Car[0]

enum Car {Bus=1, Taxi=2, Suv=3}
let taxi: String = Car[2]

enum Car {Bus=2, Taxi, Suv}
let taxi: String = Car[3]

// any (모든 타입을 저장 가능)
let str: any = "hi"
let num: any = 10

// void (함수에서 반환 값이 없는 타입) (변수에는 undefined와 null만 할당)
let unknown: void = undefined
function sayHi(): void {
   console.log("hi")
}

// never (항상 오류를 발생시키거나 절대 반환하지 않는 타입) (종료되지 않는 함수)
function neverEnd(): never {
   while (true) {}
}

function error(message: string): never {  // 항상 오류를 발생시키는 함수 정의
   throw new Error(message)
}

enum 예시

// 1
enum Car { 
    BUS = "bus", 
    TAXI = "taxi", 
    SUV = "suv" 
}
const taxi:Car = Car.TAXI
console.log(taxi); // taxi

// 2
enum Car { 
    BUS,
    TAXI,
    SUV
}
const taxi:Car = Car.TAXI
console.log(taxi); // 1

유틸리티 타입

  • 공통 타입 변환을 용이하게 하기 위해 사용

Partial

  • 프로퍼티를 선택적으로 만드는 타입을 구성
interface Todo {
  title: string
  description: string
}

function updateTodo(obj: Partial<Todo>) { 
  return obj;
}

// title or (title & description) or description 가능
const result = updateTodo({
  title: "title",
})
console.log(result); // { title: 'title' } 출력
interface User {
  id: number;
  name: string;
  age: number;   
}
let admin: Partial<User> = {
  id: 1,
  name: "Bob",
} 

// ↕ 같은 의미

interface User {
  id?: number;    // ? : 빈칸이면 undefined로 들어감
  name?: string;
  age?: number;   
}

Required

  • 모든 프로퍼티를 필수로 입력해 타입을 구성
  • 변수가 ?로 정의되었다하더라도 무조건 다 써줘야함
interface User {
  id: number;
  name: string;
  age?: number;   // ? : 빈칸이면 undefined
}

let admin1: User = { id: 1, name: "Sng" } // error 없음
let admin2: Required<User> = {
  id: 1,
  name: "Sng"
}  // error 발생 (age까지 모두 있어야함)

Readonly

  • 프로퍼티를 읽기 전용으로 설정한 타입을 구성
interface User {
  id: number;
  name: string;
  age?: number; 
}
let admin: Readonly<User> = {
  id: 1,
  name: "Bob",
}  

admin.id = 4  // 변경하려고 하면 Error 발생

Record<T, K>

  • 프로퍼티 K로 타입을 구성
  • 타입의 프로퍼티들을 다른 타입에 매핑시키는 데 사용
  • T : key, K : type
interface Score {
  "1": "A" | "B" | "C";
  "2": "A" | "B" | "C";
  "3": "A" | "B" | "C";
}

const score: Score = {
  1: "A",
  2: "B",
  3: "C",
};

console.log(score); // { '1': 'A', '2': 'B', '3': 'C' }

// ↕

type Grade = "1" | "2" | "3";
type Score = "A" | "B" | "C" | "D";

const score: Record<Grade, Score> = {
  1: "A",
  2: "B",
  3: "C",
};

console.log(score);  // { '1': 'A', '2': 'B', '3': 'C' }
interface User {
  id: number;
  name: string;
  age: number;
}

function isValid(user: User) {
  const result: Record<keyof User, boolean> = {   // keyof User: User의 키
    id: user.id > 0,
    name: user.name !== "",
    age: user.age > 0,  // 만약 age?: number로 정의하면, 이곳에서 에러가 난다. ('user.age' is possibly 'undefined'.)
  };
  return result;
}

const test: User = { id: -1, name: "Lee", age: 19 };

console.log(isValid(test));  // { id: false, name: true, age: true }
interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";  // keys

const x: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { subtitle: "home" },  // Error! 'PageInfo' 형식에 'subtitle'이(가) 없습니다
  main: {title: 'home'}  // Error! 'Record<Page, PageInfo>' 형식에 'main'이(가) 없습니다.
};

Omit<T, K>

  • 모든 프로퍼티를 선택한 다음 K 프로퍼티를 제거한 타입을 구성
interface Todo {
  title: string
  description: string
  completed: boolean
}

// Omit을 이용해 description 프로퍼티를 제외해봅니다.
type TodoPreview = Omit<Todo,"description">  // 반드시 2개의 인자

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,  
}

console.log(todo)  // { title: 'Clean room', completed: false }

Pick<T, K>

  • 프로퍼티 K의 집합을 선택해 타입을 구성
interface Todo {
  title: string;
  description: string;
  score: number;
  completed: boolean;
}

// Pick을 이용해 title 프로퍼티를 포함해봅니다..
type TodoPreview = Pick<Todo, "description" | "title" | "score">; // 반드시 2개의 인자

const todo: TodoPreview = {
  title: "Clean room",
  description: "des",
  score: 10
};

console.log(todo); // { title: 'Clean room', description: 'des', score: 10}

Exclude<T, U>

  • T에서 U에 할당할 수 있는 모든 속성을 제외한 타입을 구헝
type T0 = Exclude<"a" | "b" | "c", "a">; 
// "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"

type T2 = Exclude<string | number | (() => void), Function>;  
// string | number

Extract<T, U>

  • T에서 U에 할당할 수 있는 모든 속성을 추출하여 타입을 구성
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// "a"   (a,b,c 중에 a,f를 추출했고 f는 존재하지 않으니 a 타입만 구성

type T1 = Extract<string | number | (() => void), Function>;
// () => void  (Function 타입만 추출)

NonNullable

  • null과 undefined을 제외한 타입
type T0 = NonNullable<string | number | undefined>;
// string | number    (undefined는 제외)

type T1 = NonNullable<string[] | null | undefined>;
// string[]    (null, undefined는 제외)

함수 사용법 (JS vs TS 비교)

// js 함수 선언식
function world(name) {
  return `Hello ${name}`
}

// js 함수 표현식
let world = function(name) {
  return `Hello ${name}`
}
// ts 함수 선언식
function world(name: string): string{
  return `Hello ${name}`
}

// ts 함수 표현식
let world = function(name: string): string{
  return `Hello ${name}`
}
// js 화살표 함수 표현식
let world = (name) => {
  return `Hello ${name}`
}

// js 단축형 화살표 함수 표현식
let world = (name) => `Hello ${name}`
// ts 화살표 함수 표현식
let world = (name: string): string => {
  return `Hello ${name}`
}

// ts 단축형 화살표 함수 표현식
let world = (name: string): string => `Hello ${name}`

매개변수 유형

Parameter (기본 매개변수)

  • 함수에 주어진 인자의 수는 함수가 기대하는 매개변수의 수와 일치해야한다.
function buildName(firstName: string, lastName: string) {
  return firstName + "" + lastName;
}

let result1 = buildName("Bob"); // Error: Expected 2 arguments, but got 1
let result2 = buildName("Bob", "Adams");
let result3 = buildName("Bob","Adams","Sr."); // Error: Expected 2 arguments, but got 3

Optional Parameter (선택적 매개변수)

  • 변수명 뒤에 ‘?’를 붙이면, 선택적으로 매개변수를 사용할 수 있다. (인수가 없다면 undefined)
function buildName(firstName: string, lastName?: string) {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob");  // Bob undefined
let result2 = buildName("Bob", "Adams");  // Bob Adams
let result3 = buildName("Bob","Adams","Sr."); // Error: Expected 2 arguments, but got 3

Default Parameter (기본-초기화 매개변수)

  • default 매개변수 지정
// lastWord를 Default Parameter 형식으로 수정해주세요.
function say(firstWord: string, lastWord: string = "타입스크립트") {
  return firstWord + " " + lastWord;
}

let result1 = say("엘리스");  // 엘리스 타입스크립트
let result2 = say("엘리스", undefined);  // 엘리스 타입스크립트
let result3 = say("엘리스", "파이썬");   // 엘리스 파이썬
let result4 = say("엘리스", "파이썬", "자바");  // Error: Expected 2 arguments, but got 3

Rest Parameters (나머지 매개변수)

  • rest parameters는 그 수를 무한으로 취급
  • 아무것도 넘져주지 않을 수도 있음
function makeWord(firstChar: string, ...restOfChar: string[]) {  // string[] : 배열 Type
  return restOfChar
}

let word = makeWord("타", "입", "스", "크", "립", "트")
console.log(word) // [ '입', '스', '크', '립', '트' ]

Class


클래스 문법

  • class 키워드를 이용해 클래스를 만들 수 있습니다.
  • 클래스 내에서 클래스 멤버를 사용하기 위해서는 this 키워드를 사용합니다.
  • new 키워드를 이용해 클래스의 인스턴스를 생성합니다.

클래스 구성

  • 필드(Field) : 눈, 코, 입과 같은 신체 기관
  • 메소드(Method) : 행동
  • 생성자(Constructor) : 사람이 만들어질 때 초기값을 넣기 위한 호출
  • 멤버(Member) : 필드, 메소드, 생성자를 모두 일컫음
  • 인스턴스(Instance) : 만들어진 각각의 사람
    • new 연산자에 의해서 생성된 객체

Class 생성하기

  • 클래스 안에서 this. 를 앞에붙이면 클래스의 멤버를 의미
class Person {
  name: string
  
  constructor(name: string) {
    this.name = name
  }
  say() {
      console.log("my name is " + this.name)
  }
}

const person = new Person("엘리스")
person.say()   // my name is 엘리스

접근 제어자

  • public
    • 선언된 멤버를 클래스 밖에서도 자유롭게 접근할 수 있습니다.
    • 타입스크립트의 멤버는 기본적으로 public으로 선언되지만, 명시적으로 표시해도 됩니다.
  • protected
    • 멤버가 포함된 클래스와 그 하위 클래스에서만 접근할 수 있습니다.
  • priavte
    • 선언된 멤버를 클래스 외부에서 접근할 수 없습니다.

※ default는 없음

public vs private

class Animal {
  public name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

const ans = new Animal("Cat").name
console.log(ans)  // Cat
class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

const ans = new Animal("Cat").name
console.log(ans) // Error! 
// 'name' 속성은 'Animal' 클래스 내에서만 액세스 가

protected

class Animal {
  protected name: string

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

class Dog extends Animal {
  constructor(name: string) {
    super(name);    
  }
  
  makeSound() {
    console.log(this.name + " 멍멍!!")
  }
}

const dog = new Dog("진돗개")
dog.makeSound() // 진돗개 멍멍!! 

Getters & Setters / readonly / static

getters & setters

class의 속성에 직접 접근하는 것을 막고, gettersetter 함수를 사용해 값을 받아오거나 수정한다. 속성에 직접 접근해 수정하면 데이터 무결성이 깨질 수 있다.

클래스의 멤버에 잘못된 값을 넣어 에러를 발생시키는 것을 막기 위해 이용합니다. 예를 들어 사람 클래스에 나이 속성이 있다고 할 때, 나이는 무조건 0보다 크기 때문에 getter / setter를 설정하여 0 이하의 값이 들어오는 것을 방지할 수 있습니다.

이렇게 멤버에 직접적으로 접근하지 못하도록 하는 것을 캡슐화라고 합니다.

class Person {
  private _name: string;
  get name() {
    return this._name;
  }
  set name(name: string) {
    if (name.length > 10) {
      throw new Error("name too long");
    }
    this._name = name;
  }
}

let person = new Person();
console.log(person.name); // undefined (아직 정의되지 않았기 때문)
person.name = "june";
console.log(person.name); // june
person.name = "junejunejunejunejune"; // throw Error

readonly

속성을 읽기 전용으로 설정해 변경할 수 없게 만듭니다. 그래서 선언될 때나 생성자에 값을 설정한 이후에는 수정이 불가능한 속성입니다.

변경될 경우가 없는 상수에 해당 키워드를 이용합니다. 비슷한 기능을 하는 const가 있는데 const는 변수 참조를 위한 것이고 readonly는 속성(프로퍼티)을 위한 것입니다.

class Person {
  readonly age: number = 20; // 선언 초기화
  constructor(age: number) {
    this.age = age;
  }
}

let person = new Person(10); // 생성자 초기화
person.age = 30;  // Error! 읽기 전용 속성이므로 'age'에 할당할 수 없습니다.

static

객체마다 할당되지 않고 클래스의 모든 객체가 공유하는 멤버인 전역 멤버를 선언할 때 사용

static은 클래스 인스턴스가 아닌 클래스 이름을 이용해 접근이 가능

즉, 클래스의 인스턴스를 생성하지 않아도 호출 할 수 있다.

class Grid {
  static origin = { x: 0, y: 0 };  

  calculateDistance(): void {
    console.log(Grid.origin.x * Grid.origin.y);
  }
}

const grid = new Grid();

Grid.origin = { x: 3, y: 3 };  // 클래스명(Grid)을 이용해야 변경 가능
grid.calculateDistance(); // "9" 출력
class Foo {
  static instanceCounter = 0;
  constructor() {
    // 생성자가 호출될 때마다 카운터를 1씩 증가시킨다.
    Foo.instanceCounter++;
  }
}

console.log(Foo.instanceCounter); // 2

let foo1 = new Foo();
console.log(foo1.instanceCounter); // error

Interface

  • 타입의 이름을 짓고 코드 안의 계약을 정의한다.
  • 직접 인스턴스를 생성할 수 없고 모든 메소드가 추상 메소드이다.

interface 사용X VS interface 사용O

// interface 사용 X
function sayMyJob(obj: {job: string}) {
  console.log(obj.job);
}
const developer = { job: "개발자" }
sayName(developer)  // 개발자

// interface 사용 O
interface Person {
  job: string;
}
function sayMyJob(obj: Person) {
  console.log(obj.job);
}
const developer = {   // 또는 const developer: Person = {job: "개발자"}
  job: "개발자",
};
sayMyJob(developer); // 개발자

1️⃣ function type

// source와 subString이 인자, return값은 boolean으로 설정
interface SearchFunc {  
  (source: string, subString: string): boolean;
}

// 변수로 직접 함수 값이 할당되었기 때문에 인수 타입 생략가능
let mySearch: SearchFunc;
mySearch = function (src, sub) {
  let result = src.search(sub);
  return result > -1;    
};

// return값이 만약 string형식으로 반환된다면 에러 발생
mySearch = function (src, sub) {
  let result = src.search(sub);
  return "string";
};

2️⃣ class type

  • interface에서 정의한 추상 메소드를 각 클래스에서 구체화 시킨다.
interface Animal {
  makeSound(): void
}

// Dog class에 Animal interface를 implements합니다.
// 따라서 interface가 가지고 있는 함수를 class에서 구현해야 합니다.
class Dog implements Animal{
    makeSound(): void { console.log('멍멍')}
}

const dog = new Dog();
dog.makeSound(); // 멍멍 

3️⃣ interface 확장

  • 클래스와 마찬가지로 인터페이스도 인터페이스 간의 확장이 가능
interface Animal {
  makeSound(): void
}

// Dog 인터페이스에 Animal 인터페이스를 확장하세요.
interface Dog extends Animal{  
  run(): void
}

// BullDog class는 Dog interface를 implements했기 때문에,
// interface가 가지고 있는 함수를 class에서 구현시켜야합니다.
class BullDog implements Dog {
    makeSound(): void {
        console.log('멍멍')
    }
    run() : void {
        console.log('달리기')
    }
}

const bullDog = new BullDog();
bullDog.makeSound(); // 멍멍 
bullDog.run(); // 달리기 

4️⃣ hybrid type

  • 함수 타입이면서 동시에 객체 타입을 정의할 수 있는 인터페이스를 만들 수 있습니다
  • 아래는 Counter 인터페이스는 함수로서 호출도 가능하고, 여러 프로퍼티도 가지고 있습니다.
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
function getCounter(): Counter {
    let counter = function (start: number) {} as Counter;
    counter.interval = 123;
    counter.reset = function () {};
    return counter;
}

getCounter() 함수에 나오는 as는 타입 단언을 위한 문법으로, 말 그대로 타입을 컴파일러가 추론하지 않도록 프로그래머가 직접 지정하는 것입니다.

5️⃣ optional properties

  • property에 ? 사용
interface Config {
  color: string;
  width?: number;
}

function createSqure(config: Config): { color: string; area: number } {
  return {
    color: config.color,
    area: 100 * (config.width ? config.width : 1), 
  };
}

const config = {
  color: "red",
};

console.log(createSqure(config)); // { color: 'red', area: 100 }

다자인 패턴 - 전략패턴

  • 동적으로 행위의 수정이 필요한 경우 전략만 수정이 가능하도록 만든 패턴
    • 예를 들어 자판기 결제 방법을 현금 결제에서 카드 결제로 변경하거나 다른 새로운 결제 수단이 추가되어야할 때마다 pay메소드 구현 변경이 필요하다. 이러면 확장성이 떨어지며, OCP(개방 폐쇄)원칙에 위배됨. ※ OCP원칙 : 확장에는 열려있고, 수정에는 닫혀있어야함
    • 위 문제를 디자인 패턴으로 해결 가능. 인터페이스를 이용해 결제를 하는 것 자체는 고정적으로 두고, 결제 방법(전략)을 인터페이스를 이용해 구현해 내는 것입니다. 예를 들어 결제를 하는 행위를 implements하되 현금으로 결제하는 메소드, 카드로 결제하는 메소드를 구분해서 구현하는 것입니다. → 또다른 결제 수단이 추가되었더라도 vendingmachone 안의 pay메소드를 수정할 필요없이 또다른 클래스만 구현해서 값을 변경해준다
interface PaymentStrategy {
  pay(): void;
}

// 각 클래스의 `pay()` 메소드를 호출했을 때 cash pay, card pay가 출력되어야 합니다.
class CashPaymentStrategy implements PaymentStrategy {
  pay(): void {
    console.log("cash pay");
  }
}
class CardPaymentStrategy implements PaymentStrategy {
  pay(): void {
    console.log("card pay");
  }
}

class VendingMachine {
  private paymentStrategy: PaymentStrategy;

  // VendingMachine 인스턴스 생성될 때 기본적으로 카드 결제 전략을 사용하게 된다.
  constructor() {
    this.paymentStrategy = new CardPaymentStrategy(); // 초기값 설정
  }

  setPaymentStrategy(paymentStrategy: PaymentStrategy) {
    this.paymentStrategy = paymentStrategy;
  }

  pay() {
    this.paymentStrategy.pay();
  }
}

const vendingMachine = new VendingMachine();

vendingMachine.setPaymentStrategy(new CashPaymentStrategy());
vendingMachine.pay(); // cash pay

vendingMachine.setPaymentStrategy(new CardPaymentStrategy());
vendingMachine.pay(); // card pay

만약 기본으로 현금 결제 전략을 사용하고 싶다면, CashPaymentStrategy로 바꾼다.

constructor() {
    this.paymentStrategy = new CashPaymentStrategy(); 
}

Generic

제네릭이란?

  • 제네릭(Generic)이란 어떤 함수나 클래스가 사용할 타입을 생성 단계가 아닌 사용 단계에서 정의하는 프로그래밍 기법입니다. 즉, 타입을 명시할 때 선언 시점이 아닌 생성 시점에 명시하여 하나의 타입으로만 사용하지 않고 다양한 타입을 사용할 수 있습니다.
  • 일반적인 정적 type 언어는 함수나 클래스를 정의할 때 type을 강제적으로 선언해야 하지만, 제네릭을 이용해 코드가 수행될 때 type이 명시되도록 하는 것입니다.
  • 제네릭을 사용한다면 클래스나 메소드에서 사용할 내부 데이터 type을 외부에서 지정하여 생성 시점에 type을 명시할 수 있어 코드의 재사용률을 높일 수 있음

제네릭을 사용하는 이유

  • 제네릭을 사용하면 재사용성이 높은 함수나 클래스를 생성할 수 있습니다. 위의 echo 함수에서 제네릭을 사용하지 못했다면 타입별로 하나의 함수를 만들어야 할 것입니다. 제네릭을 사용하면 하나만 선언해도 여러 타입에서 동작이 가능합니다.
  • 제네릭을 사용하면 중복되는 코드가 줄어들고 반환되는 타입을 명시하기 때문에 코드의 가독성이 좋아집니다.
  • 만약 제네릭이 없다면 아래와 같이 any 타입을 이용해야 합니다.
    function echo2(text: any): any {
        return text;
    }
    • 하지만 any 타입은 컴파일 시 타입을 체크하지 않기 때문에 입력된 타입에 대한 메소드의 힌트를 사용할 수 없고, 컴파일 시 발견되는 에러를 발견할 수 없게됩니다.

Generic 예시

제네릭을 이용한 함수 생성

function echo<T>(text: T): T {
  return text;
}
// 정의가 아닌 생성 시점에 명시하기 때문에 여러 타입으로 정의할 수 있음
console.log(echo**<string>**("hi"));  
console.log(echo**<number>**(10));
console.log(echo**<boolean>**(true));
function sort<T>(items: T[]): T[] {
  return items.sort();
}
const nums: number[] = [1, 2, 3, 4];
const chars: string[] = ["a", "b", "c", "d"];

sort<number>(nums);
sort<string>(chars);

제네릭을 사용한 클래스 생성

class Queue<T> {
  protected data: Array<T> = [];

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

  pop(): T | undefined {
    return this.data.shift();
  }
}

const numberQueue = new Queue<number>();  // Type을 Number만 가능하게 정의

numberQueue.push(0);
numberQueue.push("1"); // Error! string' 형식의 인수는 'number' 형식의 매개 변수에 할당될 수 없습니다
numberQueue.push(+"1");  // 실수를 사전 인지하고 수정할 수 있음

Union Type

  • 유니온(Union) 타입은 어떤 타입이 올 지 경우의 수를 고려하여 타입을 명시하는 것으로 |를 이용해 선언한다. 제네릭과 마찬가지로 여러 가지 타입을 다룰 수 있다.
  • 하지만 유니온 타입의 리턴 값은 사용된 하나의 타입이 아니라 선언된 전체 유니온 타입으로 지정이 되며, 유니온 타입에서 선언한 타입들의 공통된 메소드만 사용할 수 있습니다
const printMessage = (message: string | number) => {
  return message;
}
const message1 = printMessage(1234);
const message2 = printMessage("hello world!");

message1.length; // Error! number에는 length 속성이 존재하지 않기 때문에 에러 발생
message2.length;

위 코드를 제네릭으로 바꿔보자!

const printMessage = <T>(message: T) => {
  return message;
};
const message1 = printMessage<string>("hello world!");
console.log(message1.length);  // 12

제약조건 (Constraints / keyof)

원치 않은 속성에 접근하는 것을 막기 위해 Generic에 제약조건을 사용

  • 제네릭에서 원하지 않는 속성에 접근하는 것을 막기 위해 extends 키워드를 이용해 제약조건(Constraints)을 사용할 수 있습니다.
const printMessage = <T extends string | number>(message: T): T => {
  return message;
}

printMessage<string>("1")
printMessage<number>(1)
printMessage<boolean>(false) // Error! 제약조건을 벗어나는 타입을 선언하면 에러가 발생

keyof

두 객체를 비교할 때 사용

const getProperty = <T extends object, U extends keyof T>(obj: T, key: U) => {
  return obj[key]
}

getProperty({a:1, b:2, c:3}, "a"); // 1
getProperty({a:1, b:2, c:3}, "z"); // Error! 

U의 값인 ‘z’ 가 Generic T의 키 값 중 존재하지 않기 때문에 오류가 발생

디자인 패턴 - 팩토리 패턴

  • 팩토리 패턴(Factory Pattern)이란 객체를 생성하는 인터페이스만 미리 정의하고, 인스턴스를 만드는 것을 서브 클래스가 하는 패턴입니다. 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 입력에 따라 하나의 서브 클래스의 인스턴스를 반환합니다.
// 공통
interface Car {
  drive(): void;
  park(): void;
}

class Bus implements Car {
  drive(): void {}
  park(): void {
    console.log("버스 주차");
  }
}

class Taxi implements Car {
  drive(): void {}
  park(): void {
    console.log("택시 주차");
  }
}

class Suv implements Car {
  drive(): void {}
  park(): void {
    console.log("Suv 주차");
  }
}

제너릭 사용X

class CarFactory {
  static getInstance(type: String): Car {
    // car 종류를 추가할 때마다 case문을 추가해야하는 단점이 있음
    switch (type) {
      case "bus":
        return new Bus();
      default:
        return new Taxi();
    }
  }
}

const bus = CarFactory.getInstance("bus")
const taxi = CarFactory.getInstance("taxi")

console.log(bus.park());
console.log(taxi.park());
// 버스 주차
// undefined
// 택시 주차
// undefined

제너릭 사용O

class CarFactory {
  static getInstance<T extends Car>(type: { new (): T }): T {
    return new type();
  }
}

const bus = CarFactory.getInstance(Bus)
const taxi = CarFactory.getInstance(Taxi)

console.log(bus.park());
console.log(taxi.park());
// 버스 주차
// undefined
// 택시 주차
// undefined

제너릭을 이용해 getInstance 메소드가 여러 서브 클래스를 타입으로 가질 수 있게, 즉 타입을 반환만 할 수 있게 만들고 타입을 넘겨주도록 작성한다면 새로운 타입이 추가되어도 getInstance를 수정할 필요가 없게 됩니다.

profile
안되어도 될 때까지

0개의 댓글