Array Types in TypeScript
Readability
우리는 보통 왼쪽에서 오른쪽으로 읽기 때문에 더 중요한 내용이 먼저 나와야 합니다. "이것은 문자열 배열입니다" 또는 "이것은 문자열이나 숫자 배열입니다"라고 말합니다.
left-to-right
// ✅ reads nice from left to right
function add(items: Array<string>, newItem: string)
// ❌ looks very similar to just "string"
function add(items: string[], newItem: string)
특히 배열의 유형이 상당히 긴 경우, 예를 들어 어딘가에서 추론된 경우 이 기능이 매우 중요합니다. IDE는 일반적으로 배열 유형을 배열 표기법으로 표시하기 때문에, 객체 배열 위에 마우스를 올리면 다음과 같은 메시지가 표시되는 경우가 있습니다.
options-array
const options: {
[key: string]: unknown
}[]
제 생각에는 options가 객체인 것처럼 보이는데, 맨 마지막에 가서야 실제로 배열이라는 것을 알 수 있습니다. 객체에 속성이 많으면 문제가 더 심각해집니다. 콘텐츠가 길어지고 팝오버에 스크롤바가 생겨서 마지막에 있는 []를 거의 볼 수 없게 됩니다. 물론 툴 사용 문제일 수도 있지만, 객체 배열이라고 표현한다면 그런 문제는 발생하지 않을 것입니다. 여러 줄에 걸쳐 작성해도 그다지 길지 않습니다.
array-of-options
const options: Array<{
[key: string]: unknown
}>
어쨌든, 이것이 배열 표기법의 유일한 장점이 아니기 때문에 계속 진행하겠습니다.
Readonly Arrays
솔직히 말해서, 함수의 입력으로 받는 대부분의 배열은 실수로 변경되는 것을 방지하기 위해 읽기 전용이어야 합니다. 이 주제는 별도의 글에서 다룰 예정입니다. 제네릭 표기법을 사용하는 경우 Array를 ReadonlyArray로 바꾸고 넘어가면 됩니다. 배열 표기법을 사용하는 경우 두 부분으로 나누어야 합니다.
readonly-arrays
// ✅ prefer readonly so that you don't accidentally mutate items
function add(items: ReadonlyArray<string>, newItem: string)
// ❌ "readonly" and "Array" are now separated
function add(items: readonly string[], newItem: string)
이건 그렇게 큰 문제는 아니지만, readonly가 배열과 튜플에서만 작동하는 예약어라는 건 애초에 이상합니다. 같은 기능을 하는 내장 유틸리티 타입이 있는데 말이죠. 게다가 readonly와 []를 분리하면 읽기 흐름이 정말 나빠집니다.
이 문제들은 단지 워밍업일 뿐이고, 정말 짜증 나는 부분부터 살펴보겠습니다.
Union types
add 함수를 숫자도 받도록 확장하면 어떻게 될까요? 즉, 문자열이나 숫자로 구성된 배열이 필요합니다. 제네릭 표기법에서는 문제가 없습니다.
array-of-unions
// ✅ works exactly the same as before
function add(items: Array<string | number>, newItem: string | number)
With the array notation however, things start to get weird
string-or-number-array
// ❌ looks okay, but isn't
function add(items: string | number[], newItem: string | number)
오류를 바로 발견할 수 없다면, 그것도 문제의 일부입니다. 너무 숨겨져 있어서 발견하는 데 시간이 걸립니다. 해당 함수를 실제로 구현하여 어떤 오류가 발생하는지 살펴보겠습니다.
not-assignable
// ❌ why doesn't this work 😭
function add(items: string | number[], newItem: string | number) {
return items.concat(newItem)
}
- 다음 오류가 표시됩니다.
Type 'string' is not assignable to type 'ConcatArray<number> & string' (2769)
- TypeScript 배경지식
이건 전혀 의미가 없습니다. 퍼즐을 풀려면 연산자 우선순위에 대한 것입니다. []는 | 연산자보다 바인딩이 더 강력하므로, 이제 항목을 문자열 또는 숫자[] 유형으로 만들었습니다.
우리가 원하는 것은 괄호를 포함한 (문자열 | 숫자)[]입니다. 그래야 코드가 작동합니다. 제네릭 버전은 꺾쇠괄호로 배열과 내용을 구분하기 때문에 이런 문제가 없습니다.
아직도 제네릭 구문이 더 낫다고 생각하지 않으시나요? 마지막으로 한 가지 주장이 있습니다.
keyof
객체를 받는 함수가 있고, 이 객체의 가능한 키 배열도 같은 함수에 전달하려는 꽤 흔한 예를 살펴보겠습니다. pick이나 omit과 같은 함수를 구현하려면 이 함수가 필요합니다.
pick
const myObject = {
foo: true,
bar: 1,
baz: 'hello world',
}
pick(myObject, ['foo', 'bar'])
두 번째 인수로 기존 키만 전달되도록 하고 싶은데, 어떻게 해야 할까요? keyof 타입 연산자를 사용하면 됩니다.
pick-generic-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: Array<keyof TObject>
)
물론, 배열에 일반 구문을 사용하면 모든 것이 잘 작동합니다. 하지만 배열 구문으로 바꾸면 어떻게 될까요?
pick-array-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: keyof TObject[]
)
놀랍게도 오류가 없으니 괜찮을 겁니다. 오류가 없는 건 더 심각한데, 여기에 오류가 있기 때문입니다. 함수 선언부에서는 나타나지 않고, 호출하려고 할 때 나타납니다.
pick(myObject, ['foo', 'bar'])
Argument of type 'string[]' is not assignable to parameter of type 'keyof TObject[]'.(2345)
- TypeScript 배경지식
뭐, 왜죠? 이 메시지는 이전 메시지보다 훨씬 이해가 안 됩니다. 실제 코드베이스에서 이 오류가 처음 발생했을 때, 5분 동안이나 고민했습니다. 왜 키가 문자열이 아니죠?
오류를 수정하고, 유형을 유형 별칭으로 추출하여 오류를 개선해 보려고 했지만 소용없었습니다. 그러다 문득 깨달았습니다. 괄호에 또 문제가 있는 거 아닌가요?
네, 맞습니다. 안타깝습니다. 왜 제가 그걸 신경 쓰는 걸까요? 지금까지도 keyof TObject[]에 유효한 입력이 무엇인지 모르겠습니다. 알아낼 수 없었습니다. 원하는 것을 정의하는 올바른 방법은 (keyof TObject)[]라는 것을 알고 있습니다.
fixed-array-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: (keyof TObject)[]
)
Thanks for nothing, stupid syntax.
So, I think these were all the issues I ran into when working with the array notation. I think it's sad that it's the default setting in the aforementioned eslint rule, and that most people are still preferring it - probably because of that.
It's also sad that IDEs and the TypeScript Playground show types in that notation, even if they are clearly defined differently:
type ArrayOfObject = Array<typeof myObject>
// $? ArrayOfObject = { foo: boolean; bar: number; baz: string; }[]
Maybe this article can help to convince the community that the generic notation is better, and maybe there is a way to collectively move to it as the default we're using and that we want to see. Then maybe, just maybe, tools will follow.
다차원 배열
const allCoords: Array<[number, number]> = [];
const allCoords: [number, number][] = [];
const allCoords: [number?, number?][] = []; // undefined가 들어 올경우