[TIL / DAY 26] TypeScript 심화

miseullang·2024년 11월 18일
post-thumbnail

✅ 강의 내용 정리

📍 타입 지정


함수의 타입 : 매개변수와 반환 값의 타입을 지정하는 것

function greet(name:string <- 매개변수의 타입 지정) : string <- 반환 값의 타입 지정

// 함수 선언문
  function greet(name: string): string {
    return `Hello, ${name}`;
  }

  // 함수 표현식
  const expr = function (name: string): string {
    return `Hello, ${name}`;
  }

  const expr2: (name:string) => string = function (name) {
    return `Hello, ${name}`;
  }

  greet("John");

📍 TypeScript 함수 타입


1. 함수의 기본 타입

매개변수와 반환값에 타입을 지정한다.

// 함수 선언문
function greet(name: string): string {
  return `Hello, ${name}`;
}

// 함수 표현식
const expr = function(name: string): string {
  return `Hello, ${name}`;
}

const expr2: (name: string) => string = function(name) {
  return `Hello, ${name}`;
}

2. 화살표 함수의 타입 지정

TypeScript의 강력한 타입 추론으로 첫 번째 방식을 권장한다.

2-1. 화살표 함수에 직접 타입 지정

가장 깔끔하고 일반적인 방식으로, 함수의 매개변수와 반환값의 타입을 한 번만 명시한다.

  const expr = (n1: number, n2: number): number => n1 + n2;

2-2. 변수 자체에 함수 타입을 지정하고, 화살표 함수는 타입 생략

변수의 타입을 명시적으로 선언하는 방식, 함수 타입을 변수에 지정하고, 실제 구현부는 타입 생략 가능

const expr2: (n1: number, n2: number) => number = (n1, n2) => n1 + n2;

2-3. 타입을 중복해서 지정 (과표현)

타입을 중복 선언한다. 변수 타입 선언과 함수 매개변수에 모두 타입을 지정하여 중복됨

const expr3: (n1: number, n2: number) => number = (n1: number, n2: number) =>
    n1 + n2;

3. void 타입

함수에서만 사용 가능한 특수 타입으로, 아무것도 반환하지 않는 상태를 표현한다.

function greet2(name: string): void {
  console.log(`Hello, ${name}`);
}

function processArray(arr: number[], callback: (item: number) => void) {
  for (const item of arr) {
    callback(item);
  }
}

processArray([1, 2, 3, 4], (item) => {
  console.log(item * 2);
});

4. 콜백 함수

function findInArray(
  arr: number[],
  condition: (item: number) => boolean,
  callback: (item: number) => void
) {
  for (const item of arr) {
    if (condition(item)) {
      callback(item);
      return;
    }
  }
  console.log("not found");
}

findInArray(
  [10, 20, 40, 40],
  (item) => item > 25,
  (result) => {
    console.log(`Found value: ${result}`);
  }
);

5. 클로저 패턴

반환값이 함수일 때는 (num: number) => number 형식으로 타입을 지정한다.

// 리턴값이 함수일 때는 리턴값에 대한 타입을 어떻게 지정하지?
  // : (num : number) => number (=> 이렇게 해주면 됩니당)
  function createMultipler(factor: number): (num: number) => number {
    return (num) => num * factor;
  }
  const func = createMultipler(10);
  console.log(func(2));

📍 타입 오퍼레이터


Union 타입 (|)

| : 타입의 OR 연산자 (유니온 타입)

  let value: number | string = 10;
  value = "a";

  // 각 자리에 지정되는 타입이 뭔지 타입스크립트 엔진이 정확히 알 수 있음
  let mixArr: [number, string, number, string] = [1, "a", 2, "b"];
  // 구조가 복잡해지는 경우 위치별로 타입을 지정해주기 어려움 => OR 연산으로 타입 지정
  // 추후 타입 가드 작업이 필요
  let mixArr2: (number | string)[] = [1, "a", 2, "b"];

Intersection 타입 (&)

기본 타입에서는 사용할 수 없고 객체 타입에서만 사용한다.

let value: { name: string; age: number } & { skill: string } = {
    name: "John",
    age: 20,
    skill: "front",
  };

  let value2: { name: string; age: number; skill: string } = {
    name: "John",
    age: 20,
    skill: "front",
  };
  // 인터섹션과 타입 별칭을 배우면서 활용도를 알아볼 예정

  // name이라는 속성에 두 개의 타입이 할당됐지만 타입스크립트가 에러로 잡지 않기 때문에
  // 개발자 본인의 주의를 요함
  let value3: { name: string; age: number } & { skill: string, name: number } = {
    name: "John",
    age: 20,
    skill: "front",
  };

🧑🏻‍💻 노마드코더 예습 범위

타입스크립트 공식 문서

https://www.typescriptlang.org/

자바스크립트는 런타임에 에러가 발생하기 때문에 잘못된 코드가 있다면 사용자 화면에서 에러를 발생시키지만, 타입스크립트로 작성한 파일에 에러가 나면 컴파일 자체가 되지 않는다.

즉, 개발 단계에서 대부분의 에러를 검증하고 작업을 완료할 수 있다는 것

: 을 사용해서 명시적으로 타입을 지정해주지 않아도 타입스크립트가 타입을 추론해줌

nico : 대부분의 경우 타입스크립트가 추론하도록 내버려두는 게 좋다.

📍 code


옵셔널 타입 사용하는 방법

const player: {
    name: string,
    age?: number
} = {
    name: "nico"
}

//if(player.age < 10){
// 코드를 이렇게 작성했을 때 player.age가 undefined일 수 있다고 오류를 발생시켜 알려줌
//}

if(player.age && player.age < 10){ // 이렇게 작성해야 오류를 반환하지 않음
// => player.age가 참 값이면 뒷 로직을 실행
}

클래스 방식으로 타입 지정하기

  1. 원래 이런 무식한 방법으로 타입 지정을 해줬었지만
{
  // Q1.
  let complexData: {
    id: number;
    name: string;
    details: {
      description: string;
      dimensions: {
        height: number;
        width: number;
        depth: number;
      };
      tags: string[];
    };
    reviews: [
      { user: string; rating: number; comment: string },
      { user: string; rating: number; comment: string }
    ];
  } = {
    id: 1,
    name: "Product A",
    details: {
      description: "This is a great product",
      dimensions: {
        height: 10,
        width: 5,
        depth: 2,
      },
      tags: ["sale", "new", "featured"],
    },
    reviews: [
      { user: "Alice", rating: 4.5, comment: "Excellent!" },
      { user: "Bob", rating: 3.0, comment: "Decent product." },
    ],
  };
}
  1. 클래스 방식으로 타입만 지정해두고 반복적으로 사용할 수 있음
type Player = {
  name:String,
  age?:number
}

const playerNico : Player = {
  name : "nico"
}

const playerLynn : Player = {
  name : "lynn",
  age : 12
}

읽기 전용 프로퍼티 만들기

  // readOnly 프로퍼티 만들기
  type Age = number;
  type Name = string;
  type Player = {
    readonly name: Name; // 1. readonly라는 키워드를 사용하면
    age?: Age;
  };

  const nico : Player = {
    name : "nico"
  }

  const lynn : Player = {
    name : "lynn",
    age : 12
  }

  nico.name = "seul" // 2. 이렇게 에러를 던짐

tuple 타입

array를 생성할 수 있게 하는 타입으로, 다음과 같은 조건을 가진다.
1. 최소한의 길이를 가져야 하고
2. 특정 위치에 특정 타입이 있어야 한다.

  const player : [string, number, boolean] = ["ari", 1, true];
  // 최소 3개의 값을 가지며, 차례대로 string, number, boolean여야 함을 알림

void 타입

아무것도 return 하지 않는 함수를 대상을 가리키는 것


  function hello () {
    console.log('x')
  }
}

never

함수가 절대 return하지 않을 때 발생

  1. 함수에서 예외가 발생할 때
  function hello() :never { // 1. never 키워드를 사용하면
    // return "리턴 값"; // 2. return에서 에러 발생
    throw new Error("리턴 값"); // => 이렇게 사용할 수 있음
  }
  1. 타입이 두 가지 일수도 있는 상황
  // 타입이 두 가지인 경우
  function hello(name : string | number ) {
    if (typeof name === "string") {

    } else if (typeof name === "number") {

    } else {
      name // 이렇게 하면 name은 never이 됨
    }
  }

콜 시그니처

함수의 타입을 정의하는 방법. 함수의 매개변수와 반환 타입을 미리 정의하는 것.
(함수 위에 마우스를 올렸을 때 보게 되는 것)

  // 콜 시그니처 (call signatures)
  type Add = (a: number, b: number) => number; // 콜 시그니처 정의하기
  const add: Add = (a, b) => a + b; // 내가 정의한 타입 사용하기

오버로딩

여러 개의 콜 시그니처를 가지는 함수
실무에서는 오버로딩된 함수를 직접 작성하진 않고, 라이브러리를 사용하는 편이다.
사용하려면 개념을 숙지하고 있어야 함

  // 기본적으로 콜 시그니처를 정의할 때는 화살표 함수 방식을 많이 사용함(단축키 같은 개념)
  // type Add = (a: number, b: number) => number;

  // 콜 시그니처를 길게 정의하는 방법 -> 오버로딩 된 함수를 정의할 때 사용
  // 매개변수의 타입을 여러 개 지정할 수 있음
  type Add = {
    (a: number, b: number): number;
    (a: number, b: string): number;
  };

  // const add: Add = (a, b) => a + b; // b는 숫자형도, 문자형도 될 수 있기 때문에 함수를 이렇게 작성하면 안 된다고 알려줌
  const add: Add = (a, b) => {
    if (typeof b === "string") return a
    return a + b
  };
  // 오버로딩 개념 심화 버전
  // Next.js에서 페이지 이동을 정의하는 방법
  type Config = {
    path: string;
    state: object;
  };

  type Push = {
    (path: string): void;
    (config: Config): void;
  };

  const push: Push = (config) => {
    if (typeof config === "string") {
      console.log(config);
    } else {
      console.log(config.path);
    }
  };
{
  // 오버로딩
  type Add = {
    (a: number, b: number): number;
    (a: number, b: number, c: number): number;
    // => c가 있을 수도, 없을 수도 있음을 의미
  };

  const add: Add = (a, b, c?: number) => {
    if(c) return a + b + c;
    return a + b;
  };
}

다형성(폴리모피즘)

poly : many, several, much, multi 다양한, 다각도의
morphos : form, structure 형태, 구조
=> 다양한 형태나 구조를 가지고 있는 것

 type SuperPrint = {
    (arr: number[]): void;
    (arr: boolean[]): void;
  };

  const superPrint: SuperPrint = (arr) => {
    arr.forEach((i) => {
      console.log(i);
    });
  };

  superPrint([1, 2, 3, 4]);
  superPrint([true, false, true]);
  // superPrint(["a", "b", "c"]); // 값을 처리하다보면 콜 시그니처에 정의되지 않는 타입의 값을 처리하게 되는데, 그 때마다 콜 시그니처에 추가해야 함. 너무 번거로움

제네릭

콘크리트 타입
number, string, unknown ...

제네릭 타입
타입의 placeholder같은 것

제네릭이란?
선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법

왜 제네릭을 사용하나?

콜 시그니처를 작성할 때, 내용으로 들어올 타입을 확실히 모를 때 사용

type SuperPrint = {
    <TypePlaceholder>(arr: TypePlaceholder[]): void;
    // <제네릭명>(arr: 제네릭명[]): void;
  };

  const superPrint: SuperPrint = (arr) => {
    arr.forEach((i) => {
      console.log(i);
    });
  };

  superPrint([1, 2, 3, 4]);
  superPrint([true, false, true]);
  superPrint(["a", "b", "c"]); // 제네릭 타입을 사용하면 해결
  }

** 제네릭과 any의 차이

'any'를 사용하는 것은 어떤 타입이든 받을 수 있다는 점에서 'generics'과 같지만 함수를 반환하는데 있어 'any'는 받았던 인수들의 타입을 활용하지 못한다

즉, 'generics'은 어떤 타입이든 받을 수 있다는 점에서 'any'와 같지만 해당 정보를 잃지 않고 타입에 대한 정보를 다른 쪽으로 전달할 수 있다는 점이 다르다

any 타입을 사용하면 아래 항목의 콜 시그니처가 모두 any로 뜨지만, 제네릭을 사용하면 각각 string[], boolean[] … 이 된다.

 superPrint([1, 2, 3, 4]);
 superPrint([true, false, true]);
 superPrint(["a", "b", "c"]); // 제네릭 타입을 사용하면 해결

제네릭 확장 개념
타입을 생성하고 그 타입을 또다른 타입에 넣어서 사용할 수 있다.

코드 설명 : Player라는 타입에 라는 제네릭 타입을 사용해서 정의해주고 NicoExtra를 정의한 뒤
NicoPlayer에서 그 두 개를 합쳐줌

{
  // 제네릭 확장 개념
  type Player<E> = {
    name: string;
    extraInfo: E;
  };

  type NicoExtra = {
    favFood: string;
  };

  type NicoPlayer = Player<NicoExtra>;

  const nico: NicoPlayer = {
    name: "nico",
    extraInfo: {
      favFood: "김치",
    },
  };
}

타입스크립트의 객체 지향

타입스크립트의 클래스(Class)

  • constructor의 매개변수를 지정해주면 this.firstName = firstName 같은 자바스크립트 코드로 자동 변환해준다.
  • private 키워드: 클래스 바깥에서 프로퍼티나 메서드에 접근할 수 없게 하는 키워드. 상속받은 클래스에서도 접근할 수 없다.(자바스크립트에서는 작동x)
  • protected 키워드: 자식 클래스에서는 프로퍼티나 메서드에 접근할 수 있게 하고, 외부에서는 접근할 수 없도록 하는 키워드.
  • 추상 클래스: 다른 클래스가 상속 받을 수는 있지만 새로운 인스턴스는 만들 수 없는 클래스
  • 추상 메서드: 추상 클래스 안에 만들 수 있는 메서드. 추상 클래스를 상속 받는 모든 것들이 구현 해야하는 메서드를 의미한다. 메서드를 구현해서는 안되고 call signature만 작성해야한다.

📌 접근 가능한 위치

구분선언한 클래스 내상속받은 클래스 내인스턴스
private
protected
public

TIP)

private를 사용하면 상속받은 클래스 안에서 마저도 this 사용해 접근 불가능

그래서 protected를 사용하면 상속받은 클래스 안에서 this 사용해 접근 가능

물론 protected로 지정된 것들은 외부에서 사용이 불가능

추상클래스 안에 메소드는 적어서는 안되고 call signature만 적어야 함

추상클래스 안의 메소드는 결국 구현이 되지 않는다고 나옴

  // 추상 클래스 : 다른 클래스가 상속받을 수 있는 클래스
  abstract class User {
    constructor(
      private firstName: string,
      private lastName: string,
      public nickname: string
    ) {}
    abstract getNickname() :void // 추상 메소드
    // 추상 메소드란?
    // 추상 클래스를 상속받는 모든 것들이 구현해야 하는 메소드를 의미
    private getFullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }

  // 상속받은 클래스는 오직 상속을 받을 수만 있고, 직접적으로 인스턴스를 만들지 못함
  // class Player extends User{} // getNickname을 구현하지 않아 오류 발생

  const nico = new Player("nico", "las", "니꼬");

  // nico.getFullName() // private 키워드는 프로퍼티뿐만 아니라 메소드에도 작동함

  // 타입스크립트로 해시맵(딕셔너리) 구현하기
  // 해싱 알고리즘을 쓰는 해시맵
  type Words = {
    [key:string] : string
  }
  
  class Dict {
    private words: Words;
    constructor() {
      this.words = {}
    }
    add(word:Word) {
      if(this.words[word.term] === undefined) {
        this.words[word.term] = word.def;
      }
    }
    def(term:string) {
      return this.words[term]
    }
  }

  class Word {
    constructor(
      public term:string,
      public def:string
    ) {}
  }

  const mint = new Word("choco", "민트 초코")

  const dict = new Dict();

  dict.add(mint);

💭 회고

이번에는 강의 진도가 타입스크립트로 진입하기 전에 간단히 예습을 했다.

기본 타입 지정까지는 재밌다~ 생각하면서 들었는데 클래스에 진입하면서부터 일시정지를 누르는 구간이 많아졌다.

나는 아직까지 클래스 방식이 익숙하지 않은 것 같다. 많이 사용해보면서 감을 익혀야 할듯 ㅠㅠ

이론과 별개로 코드를 짜면서 느는 실력도 있는 것 같다.

profile
괴발개발 💻

4개의 댓글

comment-user-thumbnail
2024년 11월 19일

사진 깨졋습니당

1개의 답글