본 포스팅은 "타입스크립트 프로그래밍" 책을 읽고 정리한 내용입니다.

객체

타입스크립트의 객체 타입은 객체의 형태를 정의합니다. 일반적인 객체 타입만으로는({}) 간단한 객체와(new) 복잡한 객체를 구분할 수 없습니다.

이는 자바스크립트가 구조 기반 타입(structural type)을 갖도록 설계되었기 때문입니다. 따라서 타입스크립트도 이름 기반 타입 스타일보다 자바스크립트의 스타일을 선호합니다.

구조 기반 타입화
구조 기반 타입화에서는 객체의 이름에 상관없이 객체가 어떤 프로퍼티를 갖고 있는지를 따집니다. 일부 언어에선 덕 타이핑 이라고 합니다.

타입스크리트에서 객체를 서술하는 데 타입을 이용하는 방식들을 살펴봅시다. 첫 번째 방법은 값을 object로 선언하는 것 입니다.

let a: object = {
  b: 'x' 
}

a.b // 에러 

예상했던 것과 달리 값의 프로퍼티에 접근할 수 없습니다. object는 any보다 조금 더 좁은 타입이며 서술하는 값에 관한 정보를 거의 알려주지 않고 그저 자바스크립트 객체라고 알려줄 뿐입니다.

명시적 추론이 아닌 타입스크립트의 추론은 어떨까요?

let a = {
  b: 'x'
} // {b: string}

a.b // string

let b = {
  c: {
    d: 'f'
  }
} // {c: {d: string}}

객체 리터럴 문법은 타입스크립트가 형태를 추론하거나 중괄호 안에서 명시적으로 타입을 묘사할 수 있습니다.

let a: { b: number } ={
  b: 12
} // {b: number}

const로 객체를 선언하는 경우

const a: { b: number } = {
  b: 12
} // {b: number}

지난 포스팅에서 살펴본 기본 타입과 달리 객체를 const로 선언해도 더 좁은 타입으로 추론하지 않습니다. 자바스크립트 객체의 값은 바뀔 수 있으며, 타입스크립트도 객체를 만든 후 필드 값이 바뀔 수 있다고 인지하기 때문입니다.

객체 리터럴 문법은 객체의 형태를 묘사합니다. 객체 리터럴 혹은 클래스 일 수 있습니다.

let c: {
  firstName: string
  lastName: string
} = {
  firstName: 'john',
  lastName: 'barrowman'
}

class Person {
  constructor(
  	public firstName: string, // this.firstName = ... 의 단축
    public lastName: string
  ){ }
}
c = new Person('matt', 'smith')

{firstName: string, lastName: string}은 객체의 형태를 묘사하며 객체 리터럴과 클래스 인스턴스 모두 이 형태를 만족하므로 타입스크립트는 Person을 c로 할당하는 동작을 허용합니다.

기본적으로 타입스크립트는 객체 프로퍼티에 엄격한 편입니다. 객체에 number 타입의 b라는 프로퍼티가 있어야 한다고 정의하면 타입스크립트는 오직 b만 기대합니다. 만약 b가 없거나 다른 프로퍼티가 있으면 에러를 발생시킵니다.

그럼 선택형 프로퍼티는 어떻게 표현할까요?

let a: {
  b: number
  c?: string
  [key: number]: boolean
}
  • a는 number 타입의 프로퍼티 b를 포함합니다.
  • a는 string 타입의 프로퍼티 c를 포함할 수 있습니다.
  • a는 boolean 타입의 값을 갖는 number 타입의 프로퍼티를 여러 개 포함할 수 있습니다.

    인덱스 시그니처
    [key: T]: U 같은 문법을 인덱스 시그니처라 부르며 타입스크립트에 어떤 객체가 여러 키를 가질 수 있음을 알려줍니다. 인덱스 시그니처를 이용하면 명시적으로 정의한 키 외에 다양한 키를 객체에 안전하게 추가할 수 있습니다.
    인덱스 시그니처에서 기억해야할 규칙이 있습니다. 키(T)는 반드시 number나 string 타입에 할당할 수 있는 타입어이야 한다는 것 입니다.
    인덱스 시그니처의 키 이름은 key가 아니라 원하는 이름을 사용할 수 있습니다.

    let airplaneSeatingAssignments: {
    	[seatNumber: string]: string
    } = { '34D': 'BorisCherny', '34E': 'Bill Gates' }

객체 타입을 정의할 때 필요하다면 readonly 한정자를 이용해 특정 필드를 읽기 전용으로 정의할 수 있습니다. 이는 객체 프로퍼티에 const를 적용한 듯한 효과를 냅니다.

let user: {
  readonly firstName: string
} = {
  firstName: 'abby'
}
user.firstName // string
user.firstName = 'abbey with an e' // 에러

객체 리터럴 표기법에는 빈 객체 타입({})이라는 특별한 상황이 존재합니다. null과 undefined를 제외한 모든 타입을 할당할 수 있으나, 사용을 까다롭게 만들기에 가능한 피하는 것이 좋습니다.

마지막으로 객체: Object로 객체 타입을 만드는 방법도 있습니다. {}와 비슷한 방법이며 마찬가지로 가능하면 사용하지 않아야 합니다.

타입스크립트에서 객체를 정의하는 방법은 다음처럼 네 가지로 요약할 수 있습니다.

  1. 객체 리터럴 또는 형태라 불리는 표기법({a: string}). 객체가 어떤 필드를 포함할 수 있는지 알고 있거나 객체의 모든 값이 같은 타입을 가질 때 사용한다.
  2. 빈 객체 리터럴 표기법({}). 사용하지 않는 것이 좋다.
  3. object 타입. 어떤 필드를 가지고 있는지는 관심 없고, 그저 객체가 필요할 때 사용한다.
  4. Object 타입. 사용하지 않는 것이 좋다.

일반적으로 두 번째와 네 번째 방법은 피하는 것이 좋으며, 린터를 이용해 검출하고 코드 리뷰를 하는 방식으로 방지한다.

타입 별칭, 유니온, 인터섹션

타입 별칭

변수를 선언해서 값 대신 변수로 칭하듯 타입 별칭으로 타입을 가리킬 수 있습니다.

type Age = number
type Person = {
  name: string
  age: Age
}

Age는 number입니다. 타입 별칭을 이용하면 Person의 형태를 조금 더 이해하기 쉽게 정의할 수 있습니다. 타입스크립트는 별칭을 추론하지는 않으므로 반드시 별칭의 타입을 명시적으로 정의해야 합니다.

let age: Age = 55
let driver: Person = {
  name: 'James May',
  age: age
}

Age는 number의 별칭이므로 number에 할당할 수 있습니다. 타입 별칭은 프로그램의 논리에 영향을 미치지 않고 별칭이 가리키는 타입으로 대치할 수 있습니다.

자바스크립트 변수 선언과 마찬가지로 하나의 타입을 두 번 정의할 수 없습니다. let, const처럼 타입 별칭도 블록 영역에 적용됩니다. 모든 블록과 함수는 자신만의 영역을 가지므로 내부에 정의한 타입 별칭이 외부의 정의를 덮어씁니다(shadowing).

타입 별칭은 복잡한 타입을 DRY하지 않도록 해주며 변수가 어떤 목적으로 사용되었는지 쉽게 이해할 수 있게 도와줍니다.

유니온과 인터섹션 타입

A, B라는 두 사물이 있을 때 이를 유니온하면 둘을 합친 결과가 나오며 인터섹션 하면 둘의 공통부분이 결과로 나옵니다.

유니온 타입에 사용된 값이 꼭 유니온을 구성하는 타입 중 하나일 필요는 없으며 양쪽 모두에 속할 수 있습니다.

type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean, wags: boolean}
type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog

// Cat
let a: CatOrDogBoth = {
  name: 'Bonkers',
  purrs: true
}

// Dog
a = {
  name: 'Domino',
  barks: true,
  wags: true
}

// both
a = {
  name: 'Donker',
  barks: true,
  purrs: true,
  wags: true
}

그렇다면 CatAndDog는 어떨까요? CatAndDog는 모든 것을 갖고 있습니다.

let b: CatAndDog = {
  name: 'Domino',
  barks: true,
  purrs: true,
  wags: true
}

실전에서는 대게 인터섹션보다 유니온을 자주 사용합니다.

배열

js처럼 타입스크립트 배열도 연결, 푸시, 검색, 슬라이스 등을 지원하는 객체입니다.

대개는 배열을 동형(homogeneous)으로 만듭니다. 즉, 한 배열에 사과, 오렌지, 숫자를 함께 저장하지 않고 배열의 모든 항목이 같은 타입을 갖도록 설계하려 노력합니다. 그렇지 않으면 타입스크립트에 배열과 관련한 작업이 안전한지 증명해야 하므로 추가 작업을 해야 합니다.

let a = [1,2,3] // number[]
let b = ['a', 'b'] // string[]
let c: string[] = ['a'] // string[]
let d = [1, 'a'] // (string | number) []
const e = [2, 'b'] // (string | number) []

let f = ['red']
f.push('blue')
f.push(true) // 에러

let g = [] // any[]
g.push(1) // number[]
g.push('red') // (string | number)[]

let h = [] // any[]
h.push(1) // number[]
h.push('red') // 에러

빈 배열로 초기하는 경우 타입스크립트는 배열의 요소 타입을 알 수 없으므로 any일 것으로 추측합니다. 배열을 조작하여 요소를 추가하면 타입스크립트가 주어진 정보를 이용해 배열의 타입을 추론합니다. 배열이 정의된 영역을 벗어나면(함수 안에서 배열을 선언하고 이를 반환 등) 타입스크립트는 배열을 더 이상 확장할 수 없도록 최종 타입을 할당합니다.

이처럼 상황을 쉽게 만드는 any가 있으므로 너무 복잡해할 필요는 없습니다.

튜플

튜플은 배열의 서브타입으로 길이가 고정되었고, 각 인덱스의 타입이 알려진 배열의 일종입니다. 다른 타입과 달리 튜플은 선언할 때 타입을 명시해야 합니다. 자바스크립트에서 배열과 튜플에 같은 문법을 사용하는데 타입스크립트에서는 대괄호를 배열 타입으로 추론하기 때문입니다.

let a: [number] = [1]

let b: [string, string, number] = ['malcolm', 'gladwell', 1963]

튜플은 선택형 요소도 지원합니다.

let trainFares: [number, number?][] = [
	[3.75],
    [8.25, 7.70],
    [10.50]
]

최소 길이를 갖도록 지정할 떄는 나머지 요소를 사용할 수 있습니다.

let friends = [string, ...string[]] = ['Sara', 'Tali', 'Chloe', 'Claire']

튜플은 이형 배열을 안전하게 관리할 뿐 아니라 배열 타입의 길이도 조절합니다. 이런 기능을 잘 활용하면 순수 배열에 비해 안전성을 높일 수 있습니다.

null, undefined, void, never

타입스크립트에서 null은 오직 null뿐이고 undefined는 오직 undefined뿐 이라는 점에서 특별한 타입입니다.

undefined는 아직 정해지지 않았음을 의미하는 반면 null은 값이 없다는 의미입니다.

타입스크립트는 이외에도 void, never 타입을 제공합니다. void는 명시적으로 아무것도 반환하지 않는 함수의 반환 타입(console.log 등)을 가리키며 never은 예외를 던지거나 무한루프를 실행하는 등 절대 반환하지 않는 함수 타입을 가리킵니다.

  • null - 값이 없음
  • undefined - 아직 값을 변수에 할당하지 않음
  • Void - return 문을 포함하지 않는 함수
  • never - 절대 반환하지 않는 함수

열거형

열거형은 해당 타입으로 사용할 수 있는 값을 열거하는 기법입니다. 열거형은 키를 값에 할당하는, 순서가 없는 자료구조입니다. 키가 컴파일 타임에 고정된 객체라고 생각하면 쉽습니다.

enum Language {
  English,
  Spanish,
  Russian
}

열거형은 이름은 단수 명사로 쓰고, 첫 문자는 대문자로 하는 것이 관례입니다. 키도 앞 글자를 대문자로 표시합니다.

TSC 플래그: preserveConstEnums
누군가의 코드에 정의된 const enum을 가져왔을 때 채워 넣기 기능이 문제를 일으킬 수 있습니다. 개발자가 타입스크립트 코드를 컴파일한 이후 열거형을 만든 사람이 자신의 const enum을 갱신하면 런타임에 같은 열거형이 버전에 따라 다른 값을 갖게 되고, 타입스크립트가 이 상황에서 할 수 있는 일은 없습니다.
const enum을 사용할 때는 채워 넣기 기능을 되도록 피해야 하며 제어할 수 있는 프로그램에서만 사용해야 합니다. NPM으로 배포하거나 라이브러리로 제공할 프로그램에서는 const enum을 사용하지 말아야 합니다.
const enum의 런타임 코드 생성을 활성화하려면 tsconfig.json파일에서 preserve ConstEnums TSC 설정을 true로 바꿉니다.

{
  "compilerOptions": {
    "preserveConstEnums": true
  }
}

열거형을 안전하게 사용하는 방법은 까다로우므로 열거형 자체를 멀리 하는 것이 좋습니다. 타입스크립트에는 열거형을 대체할 수단이 많습니다.

본문에서는 동료 개발자의 마음을 바꾸기 어렵다면 동료 개발자가 자리를 비웠을 때 숫자 값이나 const enum이 아닌 상황이거나 숫자 값을 받는 열거형임을 경고하도록 닌자처럼 TSLint 규칙을 머지하라고 추천합니다.(헐..)

<출처>

profile
웹 개발을 공부하고 있는 윤석주입니다.

0개의 댓글