logo

Typescript를 배웠을 때 알았으면 좋았을 3가지 트릭

[번역] 3 TypeScript Tricks I wish I knew when I learned TypeScript

typescript, translation


첫 번째: Readonly<T>

작은 예부터 시작하겠습니다: 숫자 배열을 받아서 모든 요소가 정렬된 배열을 반환하는 간단한 함수가 있습니다.

function sortNumbers(array: Array<number>) {
  return array.sort((a, b) => a - b)
}

이제 아래 코드를 보고 모든 것이 괜찮은지 확인하세요. 그리고 콘솔 결과가 어떨지 생각해보세요. 시간을 갖고 실제로 생각해보는 것을 추천합니다!

const numbers = [7, 3, 5]

const sortedNumbers = sortNumbers(numbers)

console.log(sortedNumbers)
console.log(numbers)

첫 번째 콘솔 결과는 [3, 5, 7]로 꽤 간단합니다. 하지만 두 번째 결과도 동일합니다! 그래서 여러분은 이렇게 질문할 수 있습니다: 왜 그런가요? 배열을 const로 정의했는데 어떻게 바뀔 수 있죠?

배열과 객체는 Javascript에서 매우 특별합니다. 그것들을 함수에 전달하면 배열 또는 객체에 대한 참조가 이루어지므로, in-place인 Array.sort와 같은 특정 함수를 호출하면 기존 배열이 변경됩니다.

이 상황을 구조할 Readonly 🚀

코드를 약간 바꿔 보겠습니다:

function sortNumbers(array: Readonly<Array<number>>) {
  return array.sort((a, b) => a - b)
}

이 코드는 컴파일되지 않습니다. TypeScript는 우리가 실제로 원하는 Property ‘sort’ does not exist on type ‘readonly number[]’ ‘readonly number[]’ 타입에 ‘sort’ 속성이 없습니다. 에러를 제공합니다. 부작용 side effects을 일으키는 매개변수를 변경할 수 없습니다! 멋집니다. 하지만 이것이 배열을 정렬하는 함수를 가질 수 없다는 것을 의미할까요? 물론 우린 할 수 있습니다. 배열 자체를 정렬하는 대신 배열 복사본만 정렬하면 됩니다. JS에서 배열을 복사하는 방법은 전개 ([...array]) 처럼 array.concat(), Array.from(array) 또는 array.slice()를 사용 등 다양합니다. 그럼 스프레드 연산자를 사용하여 다음과 같이 함수를 완성하겠습니다.

function sortNumbers(array: Readonly<Array<number>>) {
  return [...array].sort((a, b) => a - b)
}

끝났습니다! TypeScript에 의해 강제되는 클린 코드입니다. BTW: 이것은 객체에서도 작동합니다!

JS의 가변성에 대해 자세히 알아보려면 이 을 확인하세요.

두 번째: Any vs Unknown

TS와 함께 eslint를 사용할 때 unexpected any라는 메시지를 본 적이 있을 겁니다. 적어도 저는 any가 나쁜 이유가 궁금했습니다. 그렇지 않으면 변수가 가능한 모든 값을 가질 수 있음을 어떻게 명시해야 하나요? 여기서 예를 들어보겠습니다:

const someArray: Array<any> = []

// 몇가지 값을 추가
someArray.push(1)
someArray.push('Hello')
someArray.push({ age: 42 })
someArray.push(null)

잠재적으로 사용 가능 하고 모든 타입을 포함할 수 있는 배열을 만들고 있습니다. 최고의 코드는 아닐 수도 있지만 그냥 진행해보겠습니다. 숫자, 문자열, 객체를 추가합니다. 이제 아래 코드를 보고 어떤 일이 발생할지 생각해 봅시다:

const someArray: Array<any> = []

// ... 값을 추가
someArray.forEach((entry) => {
  console.log(entry.age)
})

이 코드는 실제로 유효한 TypeScript이며 문제 없이 컴파일됩니다. 그러나 런타임에서는 실패합니다. 왜 그렇죠? null 또는 undefined 항목을 반복하고 .age에 접근하려고 하면 다음과 같은 에러가 발생하기 때문입니다:

Uncaught TypeError: Cannot read properties of null. Uncaught TypeError: null 속성을 읽을 수 없습니다.

TS 컴파일러가 코드는 괜찮다고 말한 후라서 여러분은 제대로 작동하기를 기대하기 때문에, 이건 일종의 잘못된 보안이라고 생각합니다.

하지만 고칠 수 있습니다! 그리고 변경은 실제로 매우 간단합니다. 동일한 코드를 사용하는 경우 다음과 같이 배열을 Array<any>로 입력하는 대신 Array<unknown>을 사용하면 됩니다.

const someArray: Array<unknown> = []

// ... 값을 추가

someArray.forEach((entry) => {
  console.log(entry.age)
})

그러면 이 코드는 컴파일되지 않습니다! 대신에 TypeScript는 entry.age에 접근하려고 할 때 다음과 같은 에러를 표시합니다.

// ... 다른 코드

someArray.forEach((entry) => {
  // 객체가 'unknown' 타입입니다.
  console.log(entry.age)
})

unknown을 사용하면 값이 unknown인 것이 작업을 수행하기 전에 타입을 확인(또는 명시적으로 값 캐스팅)을 하게 강제합니다. 예를 들어보겠습니다:

// ... 다른 코드

type Human = { name: string; age: number }

someArray.forEach((entry) => {
  // 만약 객체라면 그것이 Human이라는 것을 알 수 있습니다.
  if (typeof entry === 'object') {
    console.log((entry as Human).age)
  }
})

이 사례에서 값이 객체인지 확인한 다음 .age 속성에 접근합니다. 그리고 이것은 매우 추상적인 주제이기 때문에, 여기서 간단히 줄이겠습니다:

any는 기본적으로 TypeScript 컴파일러가 해당 코드 비트를 확인하지 않는다는 의미입니다. 가능하면 any를 사용하지 마세요! 대신 unknown을 사용하는 것이 좋습니다. 값을 사용하기 전에 타입을 확인하게 강제 하기 때문입니다. 그렇지 않으면 컴파일되지 않습니다!”

참고: 유효한 객체인지 확인하는 데 typeof x === 'object'를 사용하지 마세요. 배열도 true를 반환하기 때문입니다.

세 번째: Typing Objects with Records

TS를 처음 사용하기 시작했을 때 다음과 같은 해결책을 기억할 수 없었기 때문에 항상 구글에 객체 타입 방법을 검색해야 했습니다:

interface Person {
  [key: string]: unknown
}

const Human: Person = {
  name: 'Steve',
  age: 42,
}

이것은 TS에서 객체 타입을 정하는 유효한 해결책이지만 외우기가 상당히 어렵고 또한 매우 제한적이라고 생각합니다.

예를 들어 특정 키만 허용하려면 다음과 같은 문자열 유니언을 만듭니다:

type AllowedKeys = 'name' | 'age'

interface Person {
  [key: AllowedKeys]: unknown
}

const Human: Person = {
  name: 'Steve',
  age: 42,
}

그러나 TypeScript는 이것을 좋아하지 않으며 다음과 같은 에러를 발생시킵니다:

An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead. 인덱스 시그니쳐 매개 변수 타입은 리터럴 타입 또는 제네릭 타입일 수 없습니다. 대신 매핑된 객체 타입을 사용하는 것이 좋습니다.

음, 뭐라고요? 이것은 다시 IDE를 닫고 일반 JS로 돌아가도록 만드는 TypeScript 에러 중 하나입니다. 그러나 코드를 훨씬 더 읽기 쉽게 만드는 해결책이 있습니다:

type AllowedKeys = 'name' | 'age'

// interface 대신 여기에 type을 사용하세요.
type Person = Record<AllowedKeys, unknown>

const Human: Person = {
  name: 'Steve',
  age: 42,
}

새로운 type을 정의할 수 있도록 interface에서 type으로 변경하기만 하면, 두 개의 제네릭 매개 변수를 사용하는 키워드 Record를 사용할 수 있습니다. 여기서 첫 번째는 key이고 두 번째는 해당 value입니다. 아주 간단하죠? 그런데 지금 AllowedKeys에 값을 추가하면 Human 객체에 에러가 발생합니다. 이유는 바로 그러한 속성이 없기 때문인데, 제 생각엔 꽤 멋진 것 같아요!

출처

3 TypeScript Tricks I wish I knew when I learned TypeScript

번역의 변

기존까진 한가지 문체로 번역을 해왔고 또 하고 싶었으나 경험상 하나의 문체로만 번역하기엔 매끄럽지 못하다고 판단이 들었다. 그리하여 하십시오체를 기본으로 하고 해요체를 섞어 사용했으며 앞으로도 그렇게 진행하려고 한다.