tyscript 총 복습

김내현·2025년 2월 12일

개인공부

목록 보기
45/51

TypeScript는 JavaScript에 “정적 타입”이 추가된 언어이다. 따라서 TypeScript는 JavaScript와 타입 두 가지를 모두 포함하고 있는 큰 집합, 즉 JS의 상위 집합(Superset)이라고 할 수 있다.

TypeScript 기본 개념 정리


1. 기본 타입

  • string , number , boolean , object , Array , tuple , any , null , undefined

1-1. Array 타입 ( 배열 타입)

// 기본 형태
const fruits: string[] = [`apple` , `banana` , `orange`];
// 구 버전
const fruits:Array<string> = ['apple', 'banana', 'orange'];

1-2. tuple 타입 : 특정 형태를 갖는 배열

const arr:[string, number] = ['apple', 10]; 

tuple 타입은 타입 오퍼레이터( | , & ) 를 활용하면 사실상 많이 쓰이지 않는다.

⇒ 하지만, 특정 순서와 길이를 갖춘 배열을 정의할 때는 여전히 유용하다 !

let tuple: [string, number]; // 첫 번째 요소는 string, 두 번째 요소는 number
tuple = ["hello", 42]; // ✅ 가능
tuple = [42, "hello"]; // ❌ 오류! 순서와 타입이 맞지 않음

1-3. any 타입 : 모든 타입 허용 😙

const arr:any = 10;
  • any 타입은 타입스크립트에서 유연성과 편리함을 제공하기 위해 존재하지만, 사실 타입 스크립트의 주요 장점인 타입 안정성과는 모순되는 면이 있다.

  • 하지만 개발자는 그러한 면이 필요할 때가 있다. 특히나 any 는 타입스크립트의 타입 시스템을 우회할 수 있게 해줘서 타입을 강제하지 않고 유연하게 작업을 할 수 있게 해준다.

  • 예를 들어, 개발 중에 유연한 코드를 작성해야 할 때 사용될 수 있다.

    • 외부 라이브러리 사용 시 : 타입 정의가 없거나 불완전한 외부 라이브러리를 사용할 때, 타입 스크립트가 이를 제대로 추론 하지 못할 수 있는데, 이때 any 를 사용하면 타입 체크를 하지 않고도 코드가 동작할 수 있다!
    • 빠르게 프로토타입 작성할 때 : 코드의 초기 단계에서 타입을 정확히 지정하지 않고, any 를 사용하여 빠르게 프로토타입을 작성할 수 있다!
  • 하지만!, 나중에 코드 유지보수가 어려워질 수 있고, 여러 곳에서 any 를 사용하면, 버그를 찾기 어려운 코드가 될 수 있다는 점을 명심하며, any 사용은 지양하는 것이 맞다!

⇒ 대신 unknown 를 사용할 수 있다. unknown 은 타입 체크를 거친 후에만 다른 타입으로 변환할 수 있어서, any 보다는 더 안전하게 사용할 수 있다.

let value: unknown = "hello";
value = 42; // ✅ 가능

// value를 다른 타입으로 변환하려면 타입 체크가 필요
if (typeof value === "string") {
    console.log(value.toUpperCase());  // ✅ 가능
}

1-4. void 타입 : 아무것도 반환하지 않는 것 (undefined)

  • ex) console.log() → 출력이 되는거지 반환하는 것은 아니다.

1-5. never 타입 : 절대 반환되지 않는 함수

  • ex) throw new Error(error) → 예외를 던지는 경우 (정상 종료❌)
  • 무한 루프 → 쓸 일 ❌

2. 함수 타입


함수 타입 지정은 매개변수와 반환값에 타입을 지정해주면 된다.

함수 선언문

function add(a: number, b: number): number {
  return a + b;
}

함수 표현식

함수 표현식은 타입을 2가지로 지정할 수 있다.

  1. 변수 자체에 함수 타입을 지정 ( 함수 타입 선언 )
const subtract: (a: number, b: number) => number = function (a, b) {
  return a - b;
};

장점 : 변수 타입과 함수 정의를 분리함으로, 타입만 따로 관리할 때 유용하며, 타입 오류를 조기에 발견할 수 있다.

단점 : 가독성이 떨어질 수 있다. TS가 타입 추론을 하지 않는다.

  1. 함수 자체에 타입을 지정 ( 개별 타입 지정 )
const subtract = function (a: number, b: number): number {
  return a - b;
};

장점 : 가독성이 높으며, TS의 타입 추론을 최대한 활용할 수 있다.

단점 : 함수 전체의 타입을 한 번에 확인하기 어렵다. 복잡한 함수의 경우 코드가 길어질 수 있다.

권장

  1. 복잡한 함수 타입을 정의해야 하는 경우 → 함수 타입 선언

    • 함수가 콜백으로 사용되거나, 타입을 재사용해야 하는 경우
    • 함수 타입이 길어지거나, 재사용 가능성이 있다면 타입을 분리해 관리
    type MathOperation = (a: number, b: number) => number;
    const multiply: MathOperation = (a, b) => a * b;
  1. 단순한 함수나 타입을 따로 관리할 필요가 없는 경우 → 개별 타입 지정 ( 일반적인 상황 👍)

    • 함수 정의를 깔끔하게 작성하고 싶다면 적합
    const subtract = (a: number, b: number): number => a - b;

화살표 함수

함수 표현식과 동일한 방식으로 타입을 지정

  1. 변수 자체에 타입 지정
const multiply: (a: number, b: number) => number = (a, b) => a * b;
  1. 함수 자체에 타입 지정
const multiply = (a: number, b: number): number => {
  return a * b;
};

⇒ 콜백 함수에서 명시적 타입 선언이 더 안전하고 재사용성이 좋다!

// 타입을 별도로 선언하지 않은 콜백
const numbers = [1, 2, 3];
numbers.map(num => num * 2); // 타입 추론이 자동으로 작동

// 함수 타입을 명시적으로 선언한 콜백
const double: (num: number) => number = num => num * 2;
numbers.map(double);

옵셔널 파라미터 (?) : 타입으로 지정한 매개변수를 생략할 수 있다.

3. 타입 연산자


3-1. 유니언 타입 ( | )

  • : 여러 타입 중 하나일 수 있음을 나타내는 것으로 즉, 한 변수나 함수 매개변수가 여러 다른 타입을 가질 수 있을 때 사용한다.
    let value: string | number; // string 또는 number일 수 있음
    value = "hello"; // OK
    value = 42; // OK
    value = true; // 오류: 'boolean'은 'string | number'에 할당할 수 없음

3-2. 인터섹션 타입 ( & )

  • : 여러 타입을 모두 만족해야 한다는 의미로 즉, 여러 타입을 합친 타입
    type Person = { name: string };
    type Employee = { employeeId: number };
    
    let worker: Person & Employee = {
      name: "John",
      employeeId: 1234
    }; // OK
  • 주로 객체 타입에서 많이 사용되며, 여러 개의 객체나 타입을 결합하여 하나의 객체가 모든 속성을 가질 수 있게 해준다. ⇒ 객체의 속성을 합치거나 확장할 때 매우 유용하게 사용

3-3. 유니언 타입과 타입 가드

타입 가드란?
: 코드에서 유니언 타입을 구체적인 타입으로 좁히는 방법이다. 이를 통해 타입스크립트가 특정 조건을 만족할 때 해당 타입에 맞는 속성이나 메서드를 안전하게 사용할 수 있게 해준다.

(1) typeof를 이용한 타입 가드

function printLength(value: string | number) {
  if (typeof value === "string") {
    // value는 이제 string 타입으로 좁혀짐
    console.log(value.length);
  } else {
    // value는 number 타입으로 좁혀짐
    console.log(value.toFixed(2));
  }
}

printLength("Hello");  // 출력: 5
printLength(123.456);   // 출력: 123.46

(2) instanceof를 이용한 타입 가드

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function speak(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    // animal은 Dog 타입으로 좁혀짐
    animal.bark();
  } else {
    // animal은 Cat 타입으로 좁혀짐
    animal.meow();
  }
}

const dog = new Dog();
const cat = new Cat();

speak(dog); // 출력: Woof!
speak(cat); // 출력: Meow!
🚨

타입 가드를 사용해야 하는 이유

  • 유니언 타입을 사용하는 값이 있을 때, 해당 값의 구체적인 타입을 알아야 그에 맞는 속성이나 메서드를 안전하게 사용할 수 있기 때문이다.
  • 타입 가드를 통해 값이 실제로 무엇인지를 확인하고, TS가 해당 타입에 맞는 코드로 자동으로 좁혀지게 된다.

4. 함수 오버로드


함수 오버로드를 사용한다면, 유니언 타입을 처리할 때 타입 가드가 필요 없다.

하나의 함수가 여러 개의 호출 시그니처(매개변수와 반환값 타입)을 가질 수 있게 하는 기능
⇒ 이를 통해 함수가 입력에 따라 다른 동작을 수행하도록 정의할 수 있다.

4-1. 언제 사용할까?

  • 매개변수의 타입이나 개수에 따라 다른 결과를 반환하고 싶을 때
  • 함수의 유연성을 높이고, 호출 시 타입 안정성을 보장하고 싶을 때

4-2. 구조

함수 오버로드는 2가지 부분으로 구성됩니다.

  • 오버로드 시그니처 : 함수의 다양한 사용 방법을 정의 ( 정의만, 구현❌)
  • 구현부 : 오버로드된 모든 시그니처를 처리하는 실제 함수
// 오버로드 시그니처 정의
function processInput(input: string): string; // 문자열을 입력받을 경우
function processInput(input: number): number; // 숫자를 입력받을 경우

// 구현부 정의
function processInput(input: string | number): string | number {
  if (typeof input === "string") {
    return input.toUpperCase(); // 문자열이면 대문자로 변환
  } else {
    return input * 2; // 숫자면 두 배로 반환
  }
}

// 사용
const result1 = processInput("hello"); // 'HELLO'
const result2 = processInput(10);      // 20

4-3. 주의 사항

  • 오버로드 시그니처 타입은 구현부에 포함되지 ❌
  • 구현부는 매개변수의 유니언 타입으로 작성하여 모든 시그니처를 처리해야 한다.
  • 구현부의 반환값 타입은 모든 시그니처의 반환값 타입을 포함해야 한다.

4-4. 구현부만 사용해도 타입은 지정되는 것 같은데.. 왜 쓸까?

function processInput(input: string | number): string | number {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else {
    return input * 2;
  }
}

const result1 = processInput("hello"); // 타입 추론: string | number
const result2 = processInput(10);      // 타입 추론: string | number

⇒ 그 이유 중에 하나로 호출한 결과의 타입이 항상 string | number 로 나오는 것에 있다.

  • 오버로드 시그니처를 사용하면 입력에 따라 반환 타입을 명확히 구분할 수 있다.
  • 그래서 위와 같은 결과의 타입도 항상 지정 타입만 나올 수 있다.
  • 또한, 팀 작업에서 “이 함수가 이런 입력을 받을 때 이런 결과를 내놓습니다”를 명확히 보여줄 수 있다.
  • 무엇보다 구현부만 있을 경우 호출할 때, 구체적인 입력과 출력의 관계를 타입스크립트가 명확히 알지 못해, 결과를 직접 타입 단언하거나 검사를 추가로 해야 할 수도 있다.
    const result = processInput("hello"); 
    // 타입: string | number
    // result가 string인지 number인지 타입스크립트가 확신하지 못함
    ⇒ 이로 인해 결과를 사용할 때, 추가적인 타입 검사를 해야한다.
    if (typeof result === "string") {
      console.log(result.toUpperCase());
    } else {
      console.log(result.toFixed(2));
    }
    ⇒ 위의 코드가 동작은 하지만 ,TS가 함수의 입력과 반환값의 관계를 자동으로 보장하지 않기 때문에 귀찮고 실수할 가능성이 커진다. 반면, 오버로드를 사용한다면
    // 오버로드 시그니처
    function processInput(input: string): string;
    function processInput(input: number): number;
    
    // 구현부
    function processInput(input: string | number): string | number {
      if (typeof input === "string") {
        return input.toUpperCase();
      } else {
        return input * 2;
      }
    }
    const result1 = processInput("hello"); // 타입: string
    console.log(result1.toUpperCase());   // 안전하게 사용 가능
    
    const result2 = processInput(10);     // 타입: number
    console.log(result2.toFixed(2));      // 안전하게 사용 가능
🚨

요약

  • 함수 오버로드는 입력 타입에 따라 반환 타입을 명확히 구분하고, 함수 사용의 안전성과 가독성을 높인다.
  • 구현부만 사용하면 모든 결과가 유니언 타입으로 처리되므로, 타입스크립트가 적절히 추론하지 못할 수 있기 때문에, 추가 검사가 필요할 수도 있다.
  • 오버로드는 특히 다양한 입력/출력 타입을 가진 복잡한 함수에서 유용합니다.

5. 타입 추론


타입 추론?

  • 코드 작성을 더 간결하고 읽기 쉽게 만들어 준다.
  • 반면, 타입 선언을 너무 많이 명시하면 코드가 중복처럼 느껴질 수 있고, 유지보수가 어려워진다.
// 타입 선언이 과도한 경우
const multiply: (x: number, y: number) => number = (x, y) => x * y;

// 타입 추론 활용
const multiply = (x: number, y: number) => x * y;

타입 추론 VS 타입 명시

구분타입 추론타입 명시
정의타입스크립트가 자동으로 타입을 추론개발자가 명시적으로 타입을 지정
주요 특징- 값이나 표현식을 기반으로 타입을 추론 - 타입을 자동으로 결정- 타입을 명확하게 선언 - 추론보다 우선 적용
장점- 코드가 간결해짐 - 타입을 자동으로 설정해 줌- 명확한 타입 설정 - 실수 방지
단점- 복잡한 타입의 경우 추론이 어려울 수 있음- 코드가 길어질 수 있음 - 명시해야 하는 경우가 많음

타입 추론과 명시적 선언, 무엇이 더 좋은가?

  • 단순한 함수라면 타입 추론을 활용해서 더 간결하게 작성하는 것이 좋다.
  • 하지만 복잡하거나 재사용해야 하는 함수 타입은 명시적으로 선언하는 게 더 안전하고 효율적이다.
🚨

요약

  • 타입스크립트가 타입을 정확히 추론할 수 있다면, 타입 추론을 활용하자.
  • 타입 추론이 어려운 경우에는 명시적으로 타입을 선언하자.

+) 리터럴 타입


1. 리터럴 타입이란?

: 특정 값에만 고정된 타입으로, 타입스크립트에서 값 자체가 타입이 되는 것을 의미한다.

let greeting: "hello"; // "hello"라는 값만 가질 수 있음
greeting = "hello";    // ✅ 가능
greeting = "world";    // ❌ 오류! ("hello"만 가능)

2. const 선언 과 let 선언

  • const : 값이 변경되지 않기 때문에, 타입스크립트는 이를 리터럴 타입으로 추론한다.
  • let : 값이 바뀔 가능성이 있기 때문에, 더 넓은 범주의 타입 (ex - string , number )으로 추론한다.
const fixedValue = "hello"; // 타입: "hello" (리터럴 타입)
let flexibleValue = "hello"; // 타입: string (일반 타입)

flexibleValue = "world"; // ✅ 가능 (string이기 때문)

⇒ But, let 도 타입을 명시하면, 리터럴 타입으로 사용할 수 있다.

let restrictedValue: "hello";
restrictedValue = "hello"; // ✅ 가능
restrictedValue = "world"; // ❌ 오류!

3. 리터럴 타입의 활용

: 코드에서 값의 범위를 제한하고 실수를 방지하며 안정성을 높이는 데 유용하다.

  1. 상태 관리

    type ButtonState = "enabled" | "disabled";
    let button: ButtonState = "enabled"; // "enabled" 또는 "disabled"만 가능
    button = "disabled"; // ✅ 가능
    button = "loading";  // ❌ 오류! (정의되지 않은 값)
  1. HTTP 메서드 제한

    type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
    let method: HttpMethod = "GET"; // 허용된 값만 가능
    method = "PATCH"; // ❌ 오류! ("GET", "POST", "PUT", "DELETE"만 가능)
  1. 테마 설정

    type Theme = "light" | "dark";
    let currentTheme: Theme = "light"; // "light" 또는 "dark"만 가능
    currentTheme = "dark"; // ✅ 가능
    currentTheme = "blue"; // ❌ 오류!
🚨

요약

  • 리터럴 타입은 특정 값에 고정된 타입이다.
  • const 로 선언하면 자동으로 리터럴 타입이 되지만, let 은 기본적으로 일반 타입이 된다.
  • let 은 타입 명시를 통해 리터럴 타입으로 사용할 수 있다.
  • 리터럴 타입을 사용하면 값의 범위를 제한해 안정성과 예측 가능성을 높일 수 있다.


Q&A

옵셔널 메서드와 속성의 차이 요약

특징옵셔널 메서드옵셔널 속성
호출/접근 방식호출 전에 반드시 존재 여부 확인 필요읽기만 하는 경우 타입 가드 불필요
이유호출 시 undefined 호출로 에러 발생 가능읽기 시 단순히 undefined 반환
타입 가드가 필요한 경우메서드를 호출하려 할 때값을 사용하려 할 때 (toUpperCase 등)

결론

  • 옵셔널 메서드: 호출 시 반드시 타입 가드가 필요합니다. 존재하지 않을 경우 호출 자체가 위험하기 때문입니다.
  • 옵셔널 속성: 단순히 읽는 것은 안전하므로 타입 가드가 필요하지 않습니다. 하지만 속성을 사용할 때는 undefined 가능성을 고려해야 합니다.

따라서 타입 가드의 필요 여부는 속성/메서드의 사용 맥락과 호출 방식의 안전성에 따라 결정됩니다.

0개의 댓글