Jonas Kello
Jonas Kello

Reputation: 1402

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({a: 1})
// 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 | {} | []) {
  map[key] = value
}

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.

Upvotes: 3

Views: 19047

Answers (2)

Paleo
Paleo

Reputation: 23692

With the option --strictNullChecks

I suggest:

function foo(emptyMapOrArray: { [index: string]: never } | never[]) {
    //...
}

Notice: This code requires the compiler option strictNullChecks. Otherwise, the statement foo([]) doesn't compile.

Without the option --strictNullChecks

Without the compiler option strictNullChecks, you could prefer:

function foo(emptyMapOrArray: { [index: string]: never } | undefined[]) {
    //...
}

But it's not perfect:

foo([undefined]) // no error here, but it should!

Upvotes: 2

Mikal Madsen
Mikal Madsen

Reputation: 579

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.

  • Tuples are basically objects with numeric properties. So foo: [string, string] is like saying foo: {0: string, 1: string}.

  • According to the spec section 3.3.3, the type [number, string] is equivalent to an interface that extends Array: interface NSPair extends Array<number | string> { 0: number, 1: string }

If you try to implement an interface like the latter, you will get an error:

interface EmptyArray extends Array<any> { 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<never>, 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.

Upvotes: 3

Related Questions