How To Strongly Type process.env
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
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.
Array<T>
and T[]
are functionally identical in your code.ts
constfirstTest = (arr :Array <string>) => {};constsecondTest = (arr : string[]) => {};// Both behave the same!firstTest (["hello", "world"]);secondTest (["hello", "world"]);
keyof
with T[]
can lead to unexpected results.ts
typePerson = {id : string;name : string;};constType 'string[]' is not assignable to type 'keyof Person[]'.2322Type 'string[]' is not assignable to type 'keyof Person[]'.: keyof result Person [] = ["id", "name"];
The fix is to use Array<T>
instead:
ts
constresult :Array <keyofPerson > = ["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
constarray = [1, 2];
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[]
.
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
typeTest1 = [...Array <string>, ...Array <string>];typeA rest element cannot follow another rest element.1265A rest element cannot follow another rest element.Test2 = [...string[], ...string[]];
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
typePerson = {id : string;name : string;};constType 'string[]' is not assignable to type 'keyof Person[]'.2322Type 'string[]' is not assignable to type 'keyof Person[]'.: keyof result Person [] = ["id", "name"];
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
constresult : (keyofPerson )[] = ["id", "name"];
Or, you can use Array<T>
instead:
ts
constresult :Array <keyofPerson > =["id", "name"];
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.
If you want to stay consistent with Array<T>
, you'll probably also want to use the ReadonlyArray<T>
type:
ts
constarray :ReadonlyArray <string> = ["hello", "world"];Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.array .("foo"); push
We can compare this to the readonly T[]
syntax:
ts
constarray2 : readonly string[] = ["hello", "world"];Property 'push' does not exist on type 'readonly string[]'.2339Property 'push' does not exist on type 'readonly string[]'.array2 .("foo"); push
Which do you prefer? I find this one pretty hard to differentiate.
For handling arrays of arrays, you'll also want to consider Array<Array<T>>
:
ts
constarray :Array <Array <string>> = [["hello", "world"],["foo", "bar"],];
We can compare it to the T[][]
syntax:
ts
constarray2 : string[][] = [["hello", "world"],["foo", "bar"],];
Which do you prefer?
T[]
TypeScript does offer an opinion on which it prefers. In hovers and errors, TypeScript will always use the T[]
syntax.
ts
constarray = [1, 2];constasConstArray = [1, 2] asconst ;constarrayOfArrays = [[1, 2],[3, 4],];conststringArray = ["hello", "world"];constType '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'.: number[] = numArray stringArray ;
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.
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.
Share this article with your friends
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
Discover when it's appropriate to use TypeScript's any
type despite its risks. Learn about legitimate cases where any
is necessary.
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.
Improve React TypeScript performance by replacing type & with interface extends. Boost IDE and tsc speed significantly.
In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.
Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.