[tkdodo] Array Types in TypeScript

hwisaac·2023년 9월 16일
0

tkdodos

목록 보기
1/1

https://tkdodo.eu/blog/array-types-in-type-script

"올해 초에 Matt PocockArray<string>(일반 구문) 대신 string[](배열 구문) 사용에 대한 투표를 진행했고, 그 결과가 나에게는 상당히 놀라웠습니다.

명확하게 말하자면, 두 표기법 간에 기능적인 차이는 전혀 없습니다. 이것은 선택한 표기법에 대한 개인적인 선호도로 보입니다. 어떤 방식을 선택하더라도, 반드시 array - type ESLint 규칙을 활성화하여 두 표기법 중 하나를 일관되게 사용하도록 해야 합니다.

그럼에도 불구하고 트위터에서 78% 이상의 사람들이 틀렸다고 볼 수밖에 없습니다. 나는 일반적으로 이런 절대적인 주장을 다루지 않습니다. 왜냐하면 항상 미묘한 차이와 트레이드오프가 존재하기 때문입니다. 이 경우에는 일반적인 표기법이 훨씬 나은 경우가 많다고 확신합니다.

이 질문이 제기되면 누군가가 배열 표기법을 선호하는 경우, 배열 표기법이 어느 부분에서 실패하는지와 관련된 논쟁과 사례를 보여주면 거의 즉시 납득합니다. 하지만 그에 앞서 항상 나오는 하나의 주장을 살펴보겠습니다. 그 주장은 아마도 배열 표기법을 지지하는 유일한 주장일 것입니다:

더 짧다

이것이 전부입니다. 더 작성할 문자가 적습니다. 코드를 짧게 유지하는 것이 유지 관리 가능성에 대한 좋은 지표였던 적이 있었나요? letconst보다 더 적은 문자를 가지고 있습니다. 그러면 우리는 어디서나 let을 사용해야 할까요? 그 논쟁에 대해 잘 알고 있지만 그곳으로 가지 말겁시다. 😂

하지만 진지하게 말하자면, iindex보다 짧고, ddashboard보다 짧습니다. 뭔가가 짧다고 해서 더 나은 것은 아닙니다. 우리는 코드를 쓰는 것보다 훨씬 더 자주 코드를 읽기 때문에 코드를 작성하기 쉽게 만드는 데 집중해서는 안 됩니다. 코드를 읽기 쉽게 만들어야 합니다. 그러면 제일 먼저 나오는 장점에 대해 이어지겠습니다:

가독성

보통 우리는 왼쪽에서 오른쪽으로 읽습니다. 더 중요한 것들이 먼저 와야 한다고 말합니다. "이것은 문자열의 배열입니다" 또는 "이것은 문자열 또는 숫자의 배열입니다"라고 말합니다.

왼쪽에서 오른쪽으로

올바른: 왼쪽에서 오른쪽으로 읽기 쉽게 보임

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이어야 합니다. 이에 대해 별도의 기사에서 다루고 있습니다. 일반적인 표기법을 사용하면 ArrayReadonlyArray로 대체하고 진행할 수 있습니다. 배열 표기법을 사용하는 경우 두 부분으로 나눠야 합니다:

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)

TypeScript playground

이건 나에게는 아무런 의미가 없습니다. 퍼즐을 풀기 위한 것인데요: 연산자 우선순위와 관련이 있습니다. []| 연산자보다 더 강하게 바인딩되므로 이제 itemsstring 또는 number[] 유형이 되었습니다.

아래와 같이 괄호를 사용하여 (string | number)[]를 얻고 코드를 작동시킵니다. 일반 표기법은 각각의 내용을 각각의 각도 괄호로 분리하므로 이러한 문제가 없습니다.

아직 일반적인 구문이 더 나은지 확신이 들지 않으신가요? 마지막 주장이 하나 더 있습니다:"

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 playground

무슨 일이 일어나고 있을까요? 이 메시지는 이전 것보다 더 이상한 것 같습니다. 내 키가 문자열이어야 하는데 왜 그럴까요?

왼쪽과 오른쪽으로 무언가를 바꾸어 보았고, 더 나은 오류를 얻으려고 타입 별칭에 타입을 추출하려고 했지만 성공하지 못했습니다. 그러다가 이해가 되었습니다: 또 다른 괄호 문제인가요?

네, 그렇습니다. 그리고 그것은 슬픕니다. 왜냐하면 내가 그것에 대해 왜 신경 쓰는지 모르겠기 때문입니다. 오늘까지 나는 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가 타입을 정의 방식과 상관없이 그 구문으로 표시되는 것도 슬프다:

type ArrayOfObject = Array<typeof myObject> is shown as type ArrayOfObject = { foo: boolean; bar: number; baz: string; }[]

이 글이 커뮤니티에 일반적인 표기법이 더 나은 것을 설득하는 데 도움이 되고, 우리가 사용하고 보고 싶어하는 기본 설정으로 모두 이동하는 방법이 있다면 도움이 되길 바랍니다. 그럼 아마도 도구도 따라갈 것입니다.

0개의 댓글