All Articles

Array<T> vs T[]: Which is better?

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

When you're declaring an array type in TypeScript, you've got one of two options: Array<T> or T[].

Dominik (@TKDodo on Twitter), one of the maintainers of React Query, recently posted an article on which option you should choose.

He strongly advocated for Array<T>, but I think the picture is a little more complex.

Short Explanation

  • Array<T> and T[] are functionally identical in your code.
ts
const firstTest = (arr: Array<string>) => {};
 
const secondTest = (arr: string[]) => {};
 
// Both behave the same!
firstTest(["hello", "world"]);
secondTest(["hello", "world"]);
  • Using keyof with T[] can lead to unexpected results.
ts
type Person = {
id: string;
name: string;
};
 
const result: keyof Person[] = ["id", "name"];
Type 'string[]' is not assignable to type 'keyof Person[]'.2322Type 'string[]' is not assignable to type 'keyof Person[]'.

The fix is to use Array<T> instead:

ts
const result: Array<keyof Person> = ["id", "name"];
  • Dominik argues that Array<string> is more readable than string[]. This is subjective - it's like the difference between reading "array of strings" or "string array".

  • When hovering values or displaying errors, TypeScript uses the T[] syntax. Inexperienced TS devs might experience cognitive load when translating between Array<T> in their code and T[] in their errors.

ts
const array = [1, 2];
const array: number[]
  • Overall, I disagree with Dominik that Array<T> is always the better choice. There are enough caveats to either approach that I won't be making a recommendation one way or the other.

  • But - you should be consistent. You can use this ESLint rule to enforce one or the other in your codebase. And if I had to choose, I would choose T[].

Long Explanation

No Functional Differences

Developers love a syntactical argument - especially when there is little functional difference between the two options.

Array<T> and T[] behave exactly the same, as noted above - with one small exception.

ts
type Test1 = [...Array<string>, ...Array<string>];
 
type Test2 = [...string[], ...string[]];
A rest element cannot follow another rest element.1265A rest element cannot follow another rest element.

Here, we're getting an error using the T[] syntax when trying to use it in a rest position. But even this behavior might disappear in a future TS version, as this PR demonstrates.

So, we can treat the two as functionally the same.

keyof

If you're going to make a firm judgment on which syntax to use, you need to consider the keyof operator.

As described above, keyof with T[] can lead to unexpected results.

ts
type Person = {
id: string;
name: string;
};
 
const result: keyof Person[] = ["id", "name"];
Type 'string[]' is not assignable to type 'keyof Person[]'.2322Type 'string[]' is not assignable to type 'keyof Person[]'.

You would think here that keyof Person would resolve before the [] operator kicks in, meaning you'd end up with a type like ('id' | 'name')[].

But unfortunately, the [] resolves first, so you end up performing a keyof on Person[].

You can fix this by wrapping keyof Person in parentheses:

ts
const result: (keyof Person)[] = ["id", "name"];

Or, you can use Array<T> instead:

ts
const result: Array<keyof Person> =
const result: (keyof Person)[]
["id", "name"];

Readability

Dominik argues that Array<T> is more readable than T[]. You might agree with this - but I would argue that it's subjective.

I don't want to offer an opinion here - but I want to make sure your opinion is well-informed.

Readonly Arrays

If you want to stay consistent with Array<T>, you'll probably also want to use the ReadonlyArray<T> type:

ts
const array: ReadonlyArray<string> = ["hello", "world"];
 
array.push("foo");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.

We can compare this to the readonly T[] syntax:

ts
const array2: readonly string[] = ["hello", "world"];
 
array2.push("foo");
Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.

Which do you prefer? I find this one pretty hard to differentiate.

Arrays of Arrays

For handling arrays of arrays, you'll also want to consider Array<Array<T>>:

ts
const array: Array<Array<string>> = [
["hello", "world"],
["foo", "bar"],
];

We can compare it to the T[][] syntax:

ts
const array2: string[][] = [
["hello", "world"],
["foo", "bar"],
];

Which do you prefer?

TypeScript Uses T[]

TypeScript does offer an opinion on which it prefers. In hovers and errors, TypeScript will always use the T[] syntax.

ts
const array = [1, 2];
const array: number[]
 
const asConstArray = [1, 2] as const;
const asConstArray: readonly [1, 2]
 
const arrayOfArrays = [
const arrayOfArrays: number[][]
[1, 2],
[3, 4],
];
 
const stringArray = ["hello", "world"];
 
const numArray: number[] = stringArray;
Type 'string[]' is not assignable to type 'number[]'. Type 'string' is not assignable to type 'number'.2322Type 'string[]' is not assignable to type 'number[]'. Type 'string' is not assignable to type 'number'.

This means that if you're using Array<T> in your code, less experienced TypeScript developers will experience some cognitive load translating between the two syntaxes.

This is a big reason why, to me at least, T[] feels more natural - it's more present in the language, encouraged by the compiler, and used everywhere in the docs.

Conclusion

After all of this deep thought, I don't think there's a clear winner here.

I have personally lost hours of my life trying to figure out why keyof T[] wasn't working as expected.

But I can also see a strong argument that T[] is the more intuitive choice because of how embedded it is within TypeScript as a whole.

It really comes down to one question: would I reject a PR containing the one we didn't use? No.

Use whichever you like. Use a linter to stay consistent. And don't worry about it too much.

Matt's signature

Share this article with your friends

`any` Considered Harmful, Except For These Cases

Discover when it's appropriate to use TypeScript's any type despite its risks. Learn about legitimate cases where any is necessary.

Matt Pocock
Matt Pocock

No, TypeScript Types Don't Exist At Runtime

Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.

Matt Pocock
Matt Pocock

Deriving vs Decoupling: When NOT To Be A TypeScript Wizard

In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.

Matt Pocock
Matt Pocock

NoInfer: TypeScript 5.4's New Utility Type

Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.

Matt Pocock
Matt Pocock