4 분 소요

리터럴 타입(Literal type)

일반적인 문자열과 숫자 타입 외에도, 타입 위치에서 특정한 문자열과 숫자를 참조할 수 있습니다.

리터럴 타입은 집합 타입의 보다 구체적인 하위 타입입니다. 이것이 의미하는 바는 타입 시스템 안에서 "Hello World"는 string이지만, string은 "Hello World"가 아니란 것입니다.

오늘날 TypeScript에는 문자열과 숫자, 두 가지 리터럴 타입이 있는데 이를 사용하면 문자열이나 숫자에 정확한 값을 지정할 수 있습니다.

리터럴 타입 좁히기 (Literal Narrowing)

var 또는 let으로 변수를 선언할 경우 이 변수의 값이 변경될 가능성이 있음을 컴파일러에게 알립니다. 반면, const로 변수를 선언하게 되면 TypeScript에게 이 객체는 절대 변경되지 않음을 알립니다.

// const를 사용하여 변수 helloWorld가 절대 변경되지 않음을 보장합니다.

// 따라서, TypeScript는 문자열이 아닌 "Bob"로 타입을 정합니다.
const userName1 = 'Bob'

// 반면, let은 변경될 수 있으므로 컴파일러는 문자열(string)이라고 선언할 것입니다.
let userName2 = 'Tom'

userName2 = 3 //에러, 문자열로만 바꿀 수 있다.

무한한 수의 잠재적 케이스들 (문자열 값은 경우의 수가 무한대)을 유한한 수의 잠재적 케이스 (userName1의 경우: 1개)로 줄여나가는 것을 타입 좁히기 (narrowing)라 한다.

리터럴 타입 자체로는 그다지 가치가 없습니다. 변수가 하나의 값만 가질 수 있다면 그다지 유용하지 않습니다!

그러나 리터럴을 유니언으로 결합함으로써, 훨씬 더 유용한 개념을 표현할 수 있습니다.

문자열 리터럴 타입 (String Literal Types)

실제로 문자열 리터럴 타입은 유니언 타입, 타입 가드 그리고 타입 별칭과 잘 결합됩니다. 이런 기능을 함께 사용하여 문자열로 enum과 비슷한 형태를 갖출 수 있습니다.

리터럴을 유니언으로 결합하는 예를 들면, 특정 알려진 값의 집합만 받아들이는 함수 같은 것이 있다.

예제1.

function printText(s: string, alignment: 'left' | 'right' | 'center') {
  // ...
}
printText('Hello, world', 'left')
printText("G'day, mate", 'centre')
//Argument of type '"centre"' is not assignable to parameter of
//type '"left" | "right" | "center"'.

예제2.

type Job = 'doctor' | 'developer' | 'teacher'

interface User {
  name: string
  job: Job
}

const user: User = {
  name: 'Amy',
  job: 'doctor', //Job에 없는 것을 넣으면 에러 발생
}

숫자형 리터럴 타입 (Numeric Literal Types)

TypeScript에는 위의 문자열 리터럴과 같은 역할을 하는 숫자형 리터럴 타입도 있습니다.

예제1.

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1
}

예제2.

interface HighSchoolStudent {
  name: string
  grade: 1 | 2 | 3
}

const student: HighSchoolStudent = {
  name: 'Tom',
  grade: 1,
}

유니언 타입(Union type)

TypeScript의 타입 시스템은 다양한 연산자를 사용하여 기존의 타입에서 새로운 타입을 구성할 수 있게 해줍니다. 타입을 결합하는 첫 번째 방법은 유니언 타입입니다. 유니언 타입은 두 개 이상의 다른 타입에서 형성된 타입으로, 이 타입들 중 하나일 수 있는 값을 표현합니다. 이러한 타입을 우리는 유니언의 멤버라고 합니다.

문자열이나 숫자에 대해 작동할 수 있는 함수를 작성해봅시다:

function printId(id: number | string) {
  console.log('Your ID is: ' + id)
}
// OK
printId(101)
// OK
printId('202')
// Error
printId({ myID: 22342 })
//Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.

유니언 타입 사용하기

유니언 타입에 맞는 값을 제공하는 것은 간단합니다 - 유니언의 멤버 중 하나와 일치하는 타입을 제공하면 됩니다. 유니언 타입의 값을 가지고 있다면, 어떻게 작업할까요?

TypeScript는 유니언의 모든 멤버에 대해 유효한 경우에만 연산을 허용합니다. 예를 들어, string number라는 유니언을 가지고 있다면, string에만 사용할 수 있는 메서드는 사용할 수 없습니다:
function printId(id: number | string) {
  console.log(id.toUpperCase())
}
//Property 'toUpperCase' does not exist on type 'string | number'.
//Property 'toUpperCase' does not exist on type 'number'.

코드로 유니언을 좁히는 것이 해결책입니다, 마치 타입 주석 없이 JavaScript에서 하던 것처럼 말이죠. 좁히기는 TypeScript가 코드의 구조를 기반으로 값에 대한 더 구체적인 타입을 추론할 수 있을 때 발생합니다.

예를 들어, TypeScript는 typeof 값이 “string”일 경우에만 문자열 값이 있을 것이라고 알고 있습니다:

function printId(id: number | string) {
  if (typeof id === 'string') {
    // 이 분기에서 id는 'string' 타입입니다.
    console.log(id.toUpperCase())
  } else {
    // 여기서 id는 'number' 타입입니다.
    console.log(id)
  }
}

이러한 방식으로 유니언 타입을 사용하여 더 유연하고 안전한 코드를 작성할 수 있습니다.

예제2.

interface Car {
  name: 'car'
  color: string
  start(): void
}

interface Mobile {
  name: 'mobile'
  color: string
  call(): void
}

function getGift(gift: Car | Mobile) {
  if (gift.name === 'car') {
    gift.start()
  } else {
    gift.call()
  }
}

교차타입(Intersection Type)

교차 타입은 유니언 타입과 밀접한 관련이 있지만, 사용 방법은 매우 다릅니다. 교차 타입은 여러 타입을 하나로 결합합니다. 기존 타입을 합쳐 필요한 기능을 모두 가진 단일 타입을 얻을 수 있습니다. 예를 들어, Person & Serializable & Loggable은 Person  Serializable 그리고 Loggable입니다. 즉, 이 타입의 객체는 세 가지 타입의 모든 멤버를 갖게 됩니다.

예를 들어, 일관된 에러를 다루는 여러 네트워크 요청이 있다면 해당 에러 핸들링을 분리하여 하나의 응답 타입에 대응하는 결합된 자체 타입으로 만들 수 있습니다.

예제1.

interface ErrorHandling {
  success: boolean
  error?: { message: string }
}

interface ArtworksData {
  artworks: { title: string }[]
}

interface ArtistsData {
  artists: { name: string }[]
}

// 이 인터페이스들은
// 하나의 에러 핸들링과 자체 데이터로 구성됩니다.
type ArtworksResponse = ArtworksData & ErrorHandling
type ArtistsResponse = ArtistsData & ErrorHandling

const handleArtistsResponse = (response: ArtistsResponse) => {
  if (response.error) {
    console.error(response.error.message)
    return
  }
  console.log(response.artists)
}

예제2.

interface Car {
  name: string
  start(): void
}

interface Toy {
  name: string
  color: string
  price: number
}

const toyCar: Car & Toy = {
  //Car 와 Toy를 합쳐서 toyCar를 만들어줄 수 있다.
  name: '타요',
  start() {},
  color: 'pink',
  price: 1000,
}
//모든 속성을 다 기입해줘야 한다. 그렇지 않으면 에러표시

참조

https://typescript-kr.github.io/pages/literal-types.html
https://joshua1988.github.io/ts/guide/operator.html#intersection-type
(타입스크립트 공식문서)[https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types]

댓글남기기