"올해 초에 Matt Pocock이 Array<string>
(일반 구문) 대신 string[]
(배열 구문) 사용에 대한 투표를 진행했고, 그 결과가 나에게는 상당히 놀라웠습니다.
명확하게 말하자면, 두 표기법 간에 기능적인 차이는 전혀 없습니다. 이것은 선택한 표기법에 대한 개인적인 선호도로 보입니다. 어떤 방식을 선택하더라도, 반드시 array - type ESLint 규칙을 활성화하여 두 표기법 중 하나를 일관되게 사용하도록 해야 합니다.
그럼에도 불구하고 트위터에서 78% 이상의 사람들이 틀렸다고 볼 수밖에 없습니다. 나는 일반적으로 이런 절대적인 주장을 다루지 않습니다. 왜냐하면 항상 미묘한 차이와 트레이드오프가 존재하기 때문입니다. 이 경우에는 일반적인 표기법이 훨씬 나은 경우가 많다고 확신합니다.
이 질문이 제기되면 누군가가 배열 표기법을 선호하는 경우, 배열 표기법이 어느 부분에서 실패하는지와 관련된 논쟁과 사례를 보여주면 거의 즉시 납득합니다. 하지만 그에 앞서 항상 나오는 하나의 주장을 살펴보겠습니다. 그 주장은 아마도 배열 표기법을 지지하는 유일한 주장일 것입니다:
이것이 전부입니다. 더 작성할 문자가 적습니다. 코드를 짧게 유지하는 것이 유지 관리 가능성에 대한 좋은 지표였던 적이 있었나요? let
은 const
보다 더 적은 문자를 가지고 있습니다. 그러면 우리는 어디서나 let
을 사용해야 할까요? 그 논쟁에 대해 잘 알고 있지만 그곳으로 가지 말겁시다. 😂
하지만 진지하게 말하자면, i
는 index
보다 짧고, d
는 dashboard
보다 짧습니다. 뭔가가 짧다고 해서 더 나은 것은 아닙니다. 우리는 코드를 쓰는 것보다 훨씬 더 자주 코드를 읽기 때문에 코드를 작성하기 쉽게 만드는 데 집중해서는 안 됩니다. 코드를 읽기 쉽게 만들어야 합니다. 그러면 제일 먼저 나오는 장점에 대해 이어지겠습니다:
보통 우리는 왼쪽에서 오른쪽으로 읽습니다. 더 중요한 것들이 먼저 와야 한다고 말합니다. "이것은 문자열의 배열입니다" 또는 "이것은 문자열 또는 숫자의 배열입니다"라고 말합니다.
왼쪽에서 오른쪽으로
올바른: 왼쪽에서 오른쪽으로 읽기 쉽게 보임
1// ✅ 왼쪽에서 오른쪽으로 읽기 좋음
2function add(items: Array<string>, newItem: string)
3
4// ❌ "string"만 사용한 것과 매우 유사함
5function add(items: string[], newItem: string)
특히 배열 내의 유형이 긴 경우에 특히 중요합니다. IDE는 일반적으로 배열 유형을 배열 표기법으로 표시하므로 때로는 개체 배열 위에 커서를 올리면 다음과 같이 표시됩니다:
options-array
1const options: {
2 [key: string]: unknown
3}[]
이것은 options
가 객체인 것처럼 읽히고, 마지막에야 실제로 배열임을 알 수 있습니다. 객체에 많은 속성이 있는 경우 더 길어지며 팝오버에 스크롤 막대가 생겨 []
를 보기 어려워집니다. 여러 줄에 걸쳐 표시되는 경우에도 그리 길지 않습니다:
array-of-options
1const options: Array<{
2 [key: string]: unknown
3}>
어쨌든, 이동합니다. 이것은 배열 표기법의 유일한 장점이 아닙니다.
대부분의 함수에 입력으로 가져오는 대부분의 배열은 변경을 방지하기 위해 readonly
이어야 합니다. 이에 대해 별도의 기사에서 다루고 있습니다. 일반적인 표기법을 사용하면 Array
를 ReadonlyArray
로 대체하고 진행할 수 있습니다. 배열 표기법을 사용하는 경우 두 부분으로 나눠야 합니다:
readonly-arrays
1// ✅ 항목을 실수로 변경하지 않도록 readonly를 선호합니다
2function add(items: ReadonlyArray<string>, newItem: string)
3
4// ❌ "readonly"
와 "Array"가 분리되어 있음
5function add(items: readonly string[], newItem: string)
이것은 그리 큰 문제가 아니지만, readonly
가 배열과 튜플에서만 작동하는 예약된 단어이며 같은 작업을 수행하는 내장 유틸리티 유형이 있는 경우에도 이상합니다. 그리고 readonly
와 []
를 분리하는 것은 읽기가 어렵게 만듭니다.
이 문제는 따뜻한 예고이며, 이제 정말 짜증나는 문제로 넘어가겠습니다:
add
함수를 확장하여 숫자도 허용하게 만들면 어떻게 될까요? 문자열 또는 숫자의 배열이 필요합니다. 일반적인 표기법으로는 문제가 되지 않습니다:
array-of-unions
1// ✅ 이전과 정확히 똑같이 작동합니다
2function add(items: Array<string | number>, newItem: string | number)
그러나 배열 표기법을 사용하는 경우 이해하기 어려워집니다.
string-or-number-array
1// ❌ 괜찮아 보이지만 괜찮지 않음
2function add(items: string | number[], newItem: string | number)
즉시 오류를 감지할 수 없는 경우가 있습니다. 이 문제를 해결하기 위해 실제로 함수를 구현하고 어떤 오류가 발생하는지 살펴보겠습니다:
not-assignable
1// ❌ 왜 이게 작동하지 않을까요 😭
2function add(items: string | number[], newItem: string | number) {
3 return items.concat(newItem)
4}
다음과 같은 오류가 표시됩니다:
Type 'string' is not assignable to type 'ConcatArray\ & string' (2769)
이건 나에게는 아무런 의미가 없습니다. 퍼즐을 풀기 위한 것인데요: 연산자 우선순위와 관련이 있습니다. []
는 |
연산자보다 더 강하게 바인딩되므로 이제 items
가 string
또는 number[]
유형이 되었습니다.
아래와 같이 괄호를 사용하여 (string | number)[]
를 얻고 코드를 작동시킵니다. 일반 표기법은 각각의 내용을 각각의 각도 괄호로 분리하므로 이러한 문제가 없습니다.
아직 일반적인 구문이 더 나은지 확신이 들지 않으신가요? 마지막 주장이 하나 더 있습니다:"
우리가 객체를 가져오고 이 객체의 가능한 키의 배열을 동일한 함수에 전달하려는 경우와 관련된 상당히 흔한 예제를 살펴보겠습니다. 이 경우 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)
무슨 일이 일어나고 있을까요? 이 메시지는 이전 것보다 더 이상한 것 같습니다. 내 키가 문자열이어야 하는데 왜 그럴까요?
왼쪽과 오른쪽으로 무언가를 바꾸어 보았고, 더 나은 오류를 얻으려고 타입 별칭에 타입을 추출하려고 했지만 성공하지 못했습니다. 그러다가 이해가 되었습니다: 또 다른 괄호 문제인가요?
네, 그렇습니다. 그리고 그것은 슬픕니다. 왜냐하면 내가 그것에 대해 왜 신경 쓰는지 모르겠기 때문입니다. 오늘까지 나는 keyof TObject[]
에 대한 합법적인 입력이 무엇일지 모릅니다. 이해할 수 없었습니다. 우리가 원하는 것을 정의하는 올바른 방법을 아는 것은 keyof TObject[]
가 아니라 (keyof TObject)[]
입니다.
fixed-array-notation
function pick<TObject extends Record<string, unknown>>(
object: TObject,
keys: (keyof TObject)[]
)
별로 도움이 되지 않는, 어리석은 구문에 감사합니다.
그러니까, 배열 구문을 사용할 때 마주친 모든 문제가 이것이었습니다. 위에서 언급한 eslint 규칙의 기본 설정이며 아마도 그 때문에 대부분의 사람들이 여전히 선호하는 것이 슬프다고 생각합니다.
또한 IDE와 TypeScript Playground가 타입을 정의 방식과 상관없이 그 구문으로 표시되는 것도 슬프다:
이 글이 커뮤니티에 일반적인 표기법이 더 나은 것을 설득하는 데 도움이 되고, 우리가 사용하고 보고 싶어하는 기본 설정으로 모두 이동하는 방법이 있다면 도움이 되길 바랍니다. 그럼 아마도 도구도 따라갈 것입니다.