- Typescript types for empty map and array
- 2 Answers 2
- Typing Arrays in TypeScript
- Roles of Arrays #
- Ways of typing Arrays #
- Array role “list”: array type literals vs. interface type Array #
- Array role “tuple”: tuple type literals #
- Objects that are also Array-ish: interfaces with index signatures #
- Pitfall: type inference doesn’t always get Array types right #
- Inferring types of Arrays is difficult #
- Type inference for non-empty Array literals #
- Type inference for empty Array literals #
- const assertions for Arrays and type inference #
- Typescript: How to create a non empty array Type
- How to avoid using empty arrays with Typescript?
- Type guards
- 😃 Thanks for reading!
Typescript types for empty map and array
In typescript, how can I express that a function only accepts an empty map or array? Something like this (but it does not work):
function foo(emptyMapOrArray: <> | []) < //. >// This should error foo(["a"]) foo() // This should work foo([]) foo(<>)
The use-case is that I want to be able to assign an «atomic» value to a part of a JSON document. Something like the cursor used in this paper. So a more complete example would be something like this:
function assign(map: <>, key: string, value: string | number | boolean | <> | [])
However I realised I can model the empty-map and empty-array as separate operations. But still I think it is interesting to know how expressive the type system in typescript can be in this case.
2 Answers 2
Like Paleo said, you can check for empty object with < [index: string]: never >.
However, TypeScript can only enforce the type of elements you put into an array, it cannot check the length of an array for you, so the second part of your question is impossible with TypeScript.
This issue on GitHub has some comments on it (emphasis mine):
Our view is that tuple is an array for which we have static knowledge of the individual types of the first N elements and for which the element type is the union of the types of those N elements. We allow more than N elements to be present (since there’s really not much we can do to prohibit it), but we do require those elements to have a compatible type.
When realizing that there is no checking on the number of array elements the tuple feature doesn’t seem as attractive as it did in my first impression.
According to the spec section 3.3.3, the type [number, string] is equivalent to an interface that extends Array: interface NSPair extends Array
If you try to implement an interface like the latter, you will get an error:
interface EmptyArray extends Array < 0: never; >const doesntWork: EmptyArray = [];
The error given being TS2322: Type ‘undefined[]’ is not assignable to type ‘EmptyArray’. Property ‘0’ is missing in type ‘undefined[]’.
If you attempt extends Array , the error is the same. Basically no matter how you try to wrangle the definition, you’ll get errors.
The way I understand it is that .length is a runtime property. TypeScript would have to run your code to check it, which it can’t do.
Typing Arrays in TypeScript
In this blog post, we examine how Arrays can be typed in TypeScript.
Roles of Arrays #
Arrays are used in the following two roles in JavaScript (and sometimes a mix of the two):
- Lists: All elements have the same type. The length of the Array varies.
- Tuple: The length of the Array is fixed. The elements do not necessarily have the same type.
TypeScript accommodates these two roles by offering various ways of typing arrays. We will look at those next.
Ways of typing Arrays #
Array role “list”: array type literals vs. interface type Array #
An Array type literal consists of the element type followed by [] . In the following code, the Array type literal is string[] :
const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];
An Array type literal is a shorthand for using the global generic interface type Array :
const myStringArray: Arraystring> = ['fee', 'fi', 'fo', 'fum'];
If the element type is more complicated, then you need to put them in parentheses:
In this case, I prefer Array :
Arraynumber|string> Array() => boolean>
Both array type literals and Array require all elements to have the same type. That’s how we know that they are for Arrays-as-lists.
Array role “tuple”: tuple type literals #
If the Array has a fixed length and each element has a different, fixed type that depends on its position, then we can use tuple type literals such as [string, string, boolean] :
const yes: [string, string, boolean] = ['oui', 'sí', true];
Objects that are also Array-ish: interfaces with index signatures #
If an interface has only an index signature, we can use it for Arrays:
interface StringArray < [index: number]: string; > const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];
An interface that has both an index signature and property signatures, only works for objects (because indexed elements and properties need to be defined at the same time):
interface FirstNamesAndLastName < [index: number]: string; lastName: string; > const ducks: FirstNamesAndLastName = < 0: 'Huey', 1: 'Dewey', 2: 'Louie', lastName: 'Duck', >;
Pitfall: type inference doesn’t always get Array types right #
Inferring types of Arrays is difficult #
Due to the two roles of Arrays, it is impossible for TypeScript to always guess the right type. As an example, consider the following Array literal that is assigned to the variable fields :
const fields = [ ['first', 'string', true], ['last', 'string', true], ['age', 'number', false], ];
What is the best type for fields ? The following are all reasonable options:
type Fields1 = Array<[string, string, boolean]>; type Fields2 = Array<[string, ('string'|'number'), boolean]>; type Fields3 = ArrayArraystring|boolean>> type Fields4 = [ [string, string, boolean], [string, string, boolean], [string, string, boolean], ]; type Fields5 = [ [string, 'string', boolean], [string, 'string', boolean], [string, 'number', boolean], ]; type Fields6 = [ Arraystring|boolean>, Arraystring|boolean>, Arraystring|boolean>, ]; // Etc.
Type inference for non-empty Array literals #
When we use non-empty Array literals, TypeScript’s default is to infer list types (not tuple types):
// %inferred-type: number[] const arr = [123];
Alas, that’s not always what we want:
type Pair = [string, boolean]; // %inferred-type: (str: string) => (string | boolean)[] const callback = (str: string) => [str, Boolean(str)]; // @ts-ignore: Type '(string | boolean)[][]' is not assignable to // type 'Pair[]'. // Type '(string | boolean)[]' is missing the following properties // from type 'Pair': 0, 1 (2322) const pairs: ArrayPair> = ['hello', '', '. '].map(callback);
The inferred type for callback should be a tuple type. It isn’t and that’s why its results don’t match the type parameter Pair of Array<> in the last line.
We can fix this by specifying the return type of callback explicitly (instead of relying on type inference):
const callback2 = (str: string): [string, boolean] => [str, Boolean(str)]; const pairs2: ArrayPair> = ['hello', '', '. '].map(callback2);
We also could have used Pair instead of [string, boolean] for the result of callback2 .
Type inference for empty Array literals #
If we initialize a variable via an empty Array literal, then TypeScript initially infers the type any[] and incrementally updates that type as we make changes:
const arr1 = []; // %inferred-type: any[] arr1; arr1.push(123); // %inferred-type: number[] arr1; arr1.push('abc'); // %inferred-type: (string | number)[] arr1;
Note that the initial inferred type isn’t influenced by what happens later. If we use assignment instead of .push() , things work the same:
// %inferred-type: any[] const arr1 = []; arr1[0] = 123; // %inferred-type: number[] arr1; arr1[1] = 'abc'; // %inferred-type: (string | number)[] arr1;
In contrast, if the Array literal has at least one element, then the element type is fixed and doesn’t change later:
// %inferred-type: number[] const arr = [123]; // @ts-ignore: Argument of type '"abc"' is not assignable to // parameter of type 'number'. (2345) arr.push('abc');
const assertions for Arrays and type inference #
We can suffix an Array literal with a const assertion:
// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"] const rockCategories = ['igneous', 'metamorphic', 'sedimentary'] as const;
We are declaring that categoriesOfRocks won’t change – which has the following effects:
- The Array becomes readonly : We can’t use operations that change it:
// @ts-ignore: Property 'push' does not exist on type // 'readonly ["igneous", "metamorphic", "sedimentary"]'. (2339) rockCategories.push('sand');
// %inferred-type: string[] const rockCategories2 = ['igneous', 'metamorphic', 'sedimentary'];
Here are two more examples of const assertions:
// %inferred-type: number[] const numbers1 = [1, 2, 3, 4]; // %inferred-type: readonly [1, 2, 3, 4] const numbers2 = [1, 2, 3, 4] as const; // %inferred-type: (string | boolean)[] const booleanAndString1 = [true, 'abc']; // %inferred-type: readonly [true, "abc"] const booleanAndString2 = [true, 'abc'] as const;
Typescript: How to create a non empty array Type
We are always working with arrays and in Typescript this is no exception. How can you be sure that an array is not empty just by checking its type?
Typescript is a powerful and expressive language, which allows you to avoid publishing bugs.
Remember, Typescript is not just a “super set” over Javascript, it is a “Turing complete” language in its type system, that is, you can implement complex algorithms using only types.
But its power lies in how well you express the constraints of your application, that is, how well you define your types. Typescript forces you to think differently when implementing a solution. You must think about how data flows through your program and how this information is transformed from “one form to another” That is, you must think about what types you will use. One data type we use constantly is Array .
El contenido de este sitio es y será siempre gratuito para todos. Ayudame a mantenerlo así convirtiendote en auspiciador. Tu producto o servicio podría estar aquí
In both Javascript and Typescript an array can be initialized just by using [] and you can define its type in a similar way or by using the generic form Array
// An array that can contain any tipe and will be declared empty const arr: unknown[] = [] // Another way to define the type const arr2: Arrayunknown> = [] // An array of strings defined with one element const arr3: string[] = ['str']
How to avoid using empty arrays with Typescript?
An array can then be empty or contain n elements. It is a common task to check whether or not an array is empty to operate on it. How can you determine if an array is empty? In Javascript, this task is done with a conditional block and checking the .length property of the array. But is it possible to use Typescript’s typing language to prevent an array from being empty without using a conditional? The idea here is to let Typescript check the data stream and give us an error if you’re trying to access an empty array. What you will do is create a new type similar to Array that allows you to define an array that cannot be empty by definition. Let’s call this type NonEmptyArray .
type NonEmptyArrayT> = [T, . T[]] const emptyArr: NonEmptyArrayItem> = [] // error ❌ const emptyArr2: ArrayItem> = [] // ok ✅ function expectNonEmptyArray(arr: NonEmptyArrayunknown>) { console.log('non empty array', arr) } expectNonEmptyArray([]) // you cannot pass an empty array. ❌ expectNonEmptyArray(['som valuue']) // ok ✅
So whenever you require, for example, a function parameter to be an array that cannot be empty, you can use NonEmptyArray . The only drawback is that you will now require a “type guard” function since simply checking if the length property of an array is not 0 will not transform it to type NonEmptyArray
function getArr(arr: NonEmptyArraystring>) { return arr; } const arr3 = ['1'] if (arr3.length > 0)) { // ⛔️ Error: Argument of type 'string[]' is not // assignable to parameter of type 'NonEmptyArr'. getArr(arr3); }
This error occurs because getArr expects the argument to be NonEmptyArray , but arr3 is of type Array .
Type guards
A “type-guard” function allows you to “help” Typescript correctly infer the type of some variable. It is a simple function that returns a boolean value. If this value is true then Typescript will consider the variable evaluated to be one type or another.
// Type Guard function isNonEmptyA>(arr: ArrayA>): arr is NonEmptyArrayA>{ return arr.length > 0 }
This function receives a generic array (hence the use of A ), and checks to see if the length property is greater than 0 . This function is marked to return arr is NonEmptyArray i.e. that the value of the condition evaluated is true Typescript will understand that the parameter to use arr is of type NonEmptyArray
El contenido de este sitio es y será siempre gratuito para todos. Ayudame a mantenerlo así convirtiendote en auspiciador. Tu producto o servicio podría estar aquí
// Type Guard function isNonEmptyA>(arr: ArrayA>): arr is NonEmptyArrayA>{ return arr.length > 0 } function getArr(arr: NonEmptyArraystring>) { return arr; } const arr3 = ['1'] // ^? const arr3: string[] if (isNonEmpty(arr3)) { getArr(arr3); // ^? const arr3: NonEmptyArray }
A simple way to understand type guard is that you “cast” one type to another type if and only if a certain condition is met. What makes this transformation safe compared to a simple type cast as NonEmptyArray Check out the typescript playground with these examples.
😃 Thanks for reading!
Did you like the content? Found more content like this by joining to the Newsletter or following me on Twitter