3장 고급 타입

오민준·2024년 2월 13일
0

📘 타입스크립트만의 독자적 타입 시스템

타입스크립트는 자바스크립트 자료형에서 제시되지 않은 독자적인 타입시스템을 가지고 있으나 엄밀히 말하면 타입스크립트의 타입 시스템의 개념은 모두 자바스크립트에서 기인한 것이다.
다음은 앞으로 다룰 타입의 계층 구조를 그림으로 나타낸 것이다.

💬 any 타입

  • any 타입은 자바스크립트의 기본적인 사용방식과 같으므로 자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있다.
  • 타입스크립트는 동적 타이핑 특징을 가진 자바스크립트에 정적 타이핑을 적용하는 것이 목적이나 any는 이를 무시하므로 any타입의 효용성에 대해 의문이 생긴다
  • 따라서, 타입스크립트의 사용 목적에 맞게 any 타입을 지양하는 것은 옳지만 개발 단계에서 어쩔 수 없이 any 타입을 사용해야 할 때가 있다.
    • 개발 단계에서 추후에 값이 변경될 가능성이 있거나 아직 세부적인 타입이 확정되지 않아 임시로 값을 지정할 때
    • 어떤 값을 받아올지 혹은 넘겨줄지 정할 수 없을 때
    • 값을 예측할 수 없을 때
  • any 타입이 개발자에게 편의성과 확장성을 제공하기도 하지만 남발한다면 실제 런타임에서 오류가 발생한다.

💬 unknown 타입

  • 이름처럼 아직 무엇이 할당될지 모르는 상태의 타입으로 any 타입과 유사하게 모든 타입의 값이 할당될 수 있다.
  • 함수를 unknown 타입 변수에 할당할 때는 컴파일러가 경고를 주지 않지만 실행하면 에러가 발생한다.
  • any 타입을 사용해 임시로 문제를 회피한 후 차후에 수정 사항을 누락한다면 런타임에 예상치 못한 오류가 발생하므로 unknown 타입은 이러한 상황에서 any 타입을 보완하기 위해 등장하였다.

💬 void 타입

  • 일반적으로 함수의 타입을 지정할 때, 함수에 전달되는 매개변수의 타입과 반환하는 타입을 지정해야 한다.
  • 이 때 매개변수를 전달하지 않는 경우에는 괄호를 비워두면 되지만 콘솔에 로그를 출력하거나 다른 함수를 실행하는 역할만 하는 함수의 경우 아무런 값을 반환하지 않는다.
function showModal(type: ModalType): void {
  feedbackSlice.actions.createModal(type);
  • 예시 코드와 같은 상황에서 함수 반환 타입을 void로 지정할 수 있다.
  • 함수 자체를 다른 함수의 인자로 전달하는 경우가 아니면 void 타입을 잘 명시하지 않는 경향이 있는데, 이는 함수 내부에 별도의 반환문이 없다면 컴파일러가 함수 타입을 void로 추론해주기 때문이다.

💬 never 타입

  • never 타입은 값을 반환할 수 없는 타입을 말한다. 이 때, 값을 반환하지 않는 것과 값을 반환할 수 없는것을 명확히 구분해야 한다.
  • 값을 반환할 수 없는 예에는 크게 두가지가 있다.
    • throw 키워드를 사용해 에러를 던지는 경우
    • 무한히 함수가 실행되는 경우

💬 Array 타입

  • typeof를 사용하여 타입을 알 수 있지만 Object.prototype.toString.call() 함수를 사용하면 객체의 인스턴스까지 알려준다.
  • 대괄호([ ])를 사용하여 직접 타입을 명시할 수 있는데 이 때의 타입은 배열보다 좁은 범위인 tuple을 가리킨다.
  • 배열은 사전에 허용하지 않은 타입이 서로 섞이는 것을 방지하여 타입 안정성을 제공하고 튜플은 길이까지 제한하여 원소 갯수와 타입을 보장한다.
  • 어떤 값이던 배열의 원소로 허용하지만 이는 타입스크립트의 정적 타이핑과 부합하지 않으므로 배열의 원소로 하나의 타입만 사용해야한다.
const array: number[] = [1, 2, 3]; // 숫자에 해당하는 원소만 허용한다.
const array: Array<number> = [1, 2, 3] // 표기만 다른 동일한 선언 방식이다.
  • 여러 타입을 관리해야 하는 배열을 선언하려면 유니온 타입을 활용할 수 있다.
const array1: Array<number | string> = [1, "string"];
const array2: number [] | string[] = [1, "string"];
const array3: (number | string)[] = [1, "string"];
  • tuple은 배열의 특정 인덱스에 정해진 타입을 선언하는 것이다.
  • 또한 옵셔널 프로퍼티를 명시하고 싶다면 물음표 기호와 함께 해당 속성을 선언하면 된다.
let tuple: [number] = [1];
tuple = [1, 2]; // 불가능
tuple = [1, "string"]; // 불가능
let tuple: [number, string, boolean] = [1, "string", true]; // 여러 타입과 혼합 가능

let optionlaTuple1: [number, number?] = [1]
let optionlaTuple1: [number, number?] = [1, 2]
  • 대표적인 tuple의 유용한 쓰임새는 리액트의 useState이다.
  • useState API는 배열 원소의 자리마다 명확한 의미를 부여하므로 컴포넌트에서 사용하지 않은 값에 접근하는 오류를 방지할 수 있다.
  • 또한 구조분해 할당을 이용하여 사용자가 자유롭게 이름을 지정할 수 있다.
import { useState } from "react"

const [value, setValue] = useState(false)
const [username, setUsername] = useState("")

💬 enum 타입

  • enum 타입은 열거형으로 기본적인 추론 방식은 숫자 0부터 1씩 늘려가며 값을 할당하는 것이다.
  • enum 타입은 주로 문자열 상수를 생성하는데 사용한다.
  • 그러나 할당된 값을 넘어 역방향으로 접근하는 경우 타입스크립트는 막지 않으므로 안전하지 않다. 이러한 동작을 막기 위해 const enum으로 열거형을 선언하는 방법이 있다.
  • const enum은 역방향으로의 접근을 허용하지 않으므로 자바스크립트에서 객체에 접근하는 것과 유사한 동작을 보장한다.
enum ProgrammingLanguage {
  Typescript = "Typescript", // 각 멤버에 명시적으로 값을 할당할 수 있다.
  Javascript = "300",
  Java,
  Python,
  Kotlin,
  Rust,
  Go,
}

ProgrammingLanguage[2] // Java
ProgrammingLanguage[200] // undefined를 출력하나 에러는 발생하지 않는다.

// 이와 같이 선언하면 위의 문제를 방지할 수 있다.
const enum ProgrammingLanguage {
  ...
}
  • 그러나 숫자 상수로 관리되는 열거형은 const enum으로 열거형을 선언하더라도 선언한 값 이외의 값을 할당하거나 접근할 때 이를 방지하지 못한다.
  • 따라서 의도하지 않은 값의 할당이나 접근을 방지하는 문자열 상수 방식이 숫자 상수 방식보다 더 안전하다.
  • 열거형은 타입스크립트 코드가 자바스크립트로 변환될 때 즉시실행 함수 형식으로 변환되는데, 일부 번들러에서 트리쉐이킹 과정 중 즉시 실행 함수로 변환된 값을 사용하지 않는 경우가 발생한다.
  • 이러한 문제를 해결하려면 const enum 또는 as const assertion을 사용해 유니온 타입으로 열거형과 동일한 효과를 얻는 방법이 있다.

📗 타입 조합

앞에서 다룬 개념을 응용하거나 약간의 내용을 덧붙여 좀 더 심화한 타입 검사를 수행하는데 필요한 지식을 살펴본다.

💬 교차 타입

  • 교차 타입을 사용하여 여러가지 타입을 결합, 하나의 단일 타입으로 만들 수 있다.
  • & 를 사용하여 표기하며 type C = A & B 라면 타입 C는 타입 A와 타입 B의 모든 멤버를 가진 타입이다.
type ProductItem = {
  id: number
  name: string
  type: string
  price: number
  imageUrl: string
  quantity: number
}

type ProductItemwithDiscount = Product & {discountAmount: number }

💬 유니온 타입

  • 유니온 타입은 타입 A 또는 타입 B 둘 중 하나가 될 수 있는 타입을 말한다.
  • A | B 로 표기한다.
type CardItem = {
  id: number
  name: string
  type: string
  imageUrl: string
}

// PromotionEventItem이 ProductItem 일 수도 CardItem일 수도 있다.
type PromotionEventItem = ProductItem | CardItem

const printPromotionItem = (item: PromotionEvenItem) => {
  console.log(item.name) // 0
  console.log(item.quantity) // 컴파일 에러 발생

💬 인덱스 시그니처

  • 인덱스 시그니쳐는 특정 타입의 속성 이름은 알 수 없지만 속성의 타입을 알고 있을 때 사용한다.
  • [key: K]: T 꼴로 타입을 명시하는데 해당 타입의 속성 키는 모두 K 타입이고 속성 값은 모두 T 타입을 가져야 한다는 의미이다.
interface IndexSignatureEx {
  [key: string]: number
  • 인덱스 시그니쳐를 선언할 때 다른속성을 추가로 명시해줄 수 있는데 이 때 추가로 명시된 속성은 인덱스 시그니처에 포험되는 타입이여야 한다.
interface IndexSignatureEx2 {
  [key: string]: number | boolean
  length: number
  isValid: boolean
  name: string // 에러 발생

💬 인덱스드 엑세스 타입

  • 다른 타입의 특정 속성이 가지는 타입을 조회하기 위해 사용하거나 배열의 요소 타입을 조회하기 위해 사용한다.

💬 맵드 타입

  • 인덱스 시그니처 문법을 사용해 반복적인 타입 선언을 줄일 수 있다.

💬 템플릿 리터럴 타입

  • 자바스크립트의 템플릿 리터럴 문자열을 사용하여 문자열 리터럴 타입을 선언할 수 있는 문법이다.
type Stage = | "init" | "select-image" | "edit-image"
type StageName = `${Stage}-stage` // 'init-stage' | 'select-image-stage' | 'edit-image-stage'

💬 제네릭

  • 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워둔 다음, 실제고 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식이다.
  • 해당 방식을 사용하면 함수, 타입, 클래스 등 여러 타입에 대해 따로 정의하지 않아도 되기 때문에 재사용성이 크게 향상된다.
  • 타입 변수는 꺽쇠괄호 내부에 정의되며 T(Type), E(Element), K(Key), V(Value)등이 사용된다.
type ExampleArrayType<T> = T[]

const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"]

📙 제네릭 사용법

💬 함수의 제네릭

  • 어떤 함수의 매개변수나 반환값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있다.
function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string):
Repository<T> {
  return getConnection("ro").getRepository(target)
}

💬 호출 시그니처의 제네릭

  • 호출 시그니처는 타입스크립트의 함수 문으로 매개변수와 반환타입을 미리 선언하여 함수 호출시 필요한 타입을 별도로 지정하는 것을 말한다.
  • 제네릭 타입을 어디에 위치시키는지에 따라 타입의 범위와 제네릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있다.

💬 제네릭 클래스

  • 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다.
  • 클래스 이름 뒤에 타입 매개변수인 < T >를 선언해준다.
  • 제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용되므로 특정 메소드를 대상으로 제네릭을 적용하려면 해당 메소드를 제네릭 메소드로 선언하면 된다.

💬 제한된 제네릭

💬 확장된 제네릭

💬 제네릭 예시

  • 제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
  • 실제 현업에서는 API 응답값의 타입을 지정할 때이다.
// API 응답값에 따라 달라지는 data를 제네릭 타입 Data로 선언하고 있다.
export interface MobileApiResponse<Data> {
  data: Data
  statusCode: string
  statusMessage? : string
} 

// 이렇게 만든 MobileAPiResponse는 실제 API 응답 값의 타입을 지정할 때 아래와 같이 사용된다.

export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
  const priceUrl = "https://...." // url 주소
  
  return request({
    method: "GET",
    url: priceUrl,
  })
}

export const fetchOrderInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
  const orderUrl = "https://...." // url 주소
  
  return request({
    method: "GET",
    url: orderUrl,
  })
}

  • 제네릭은 코드의 재사용성을 높이고 타입 추론을 하는데 사용되므로 any를 사용하면 그 이점을 누릴 수 없게 된다.
  • 무분별한 제네릭 사용한 가독성을 해치므로 부득이한 상황을 제외하고 의미 단위로 분할해서 사용하는게 좋다.
profile
ChatGPT-Driven Development를 지양합니다.

0개의 댓글