Typescript를 배웠을 때 알았으면 좋았을 3가지 트릭
[번역] 3 TypeScript Tricks I wish I knew when I learned TypeScript
첫 번째: 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
번역의 변
기존까진 한가지 문체로 번역을 해왔고 또 하고 싶었으나 경험상 하나의 문체로만 번역하기엔 매끄럽지 못하다고 판단이 들었다. 그리하여 하십시오체를 기본으로 하고 해요체를 섞어 사용했으며 앞으로도 그렇게 진행하려고 한다.