11

I'm trying to define a type that matches any object/dictionary but NOT arrays.

My first attempt didn't work since arrays are technically objects under the hood:

const a:{[k:string]: any} = []; // works fine

I also know that it's possible to create a generic "checker" like so:

type NoArray<T> = T extends any[] ? never : T;

But that's not what I'm looking for. I want a non-generic type that works like this:

const a: NoArrayType = {}; // works fine
const a: NoArrayType = []; // TypeError

7 Answers 7

7

Here's what I came up with, using a type intersection instead of an index signature:

/**
 * Constrains a type to something other than an array.
 */
export type NotArray = (object | string | bigint | number | boolean) & { length?: never; };

This allows more than what the original poster was looking for, but it can easily be adjusted:

/**
 * Constrains a type to an object other than an array.
 */
export type NonArrayObject = object & { length?: never; };

The advantage of not using an index signature is that you get an error if you access a property which doesn't exist:

function test(hello: NonArrayObject) {
    return hello.world; // error
}

The disadvantages of using … & { length?: never; } are that you can access the length property of NotArray, that you cannot use it for objects which happen to have a length property, and that you cannot use it for functions because they have a length property.

And if anyone is wondering, I use NotArray to define optional return values, where in most cases only the first return value is of interest:

export type OptionalReturnValues2<T1 extends NotArray, T2> = T1 | [T1 | undefined, T2];

export function normalizeReturnValues2<T1 extends NotArray, T2>(optionalReturnValues: OptionalReturnValues2<T1, T2>): [T1 | undefined, T2 | undefined] {
    if (Array.isArray(optionalReturnValues)) {
        return optionalReturnValues;
    } else {
        return [optionalReturnValues, undefined];
    }
}

export type OptionalReturnValues3<T1 extends NotArray, T2, T3> = T1 | [T1 | undefined, T2] | [T1 | undefined, T2 | undefined, T3];

export function normalizeReturnValues3<T1 extends NotArray, T2, T3>(optionalReturnValues: OptionalReturnValues3<T1, T2, T3>): [T1 | undefined, T2 | undefined, T3 | undefined] {
    if (Array.isArray(optionalReturnValues)) {
        return [optionalReturnValues[0], optionalReturnValues[1], optionalReturnValues[2]];
    } else {
        return [optionalReturnValues, undefined, undefined];
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

An object could have the length property and this solution would fail completely
6

Type problem is the any in your type declaration. any is usually something you want to avoid in most typescript applications.

An array is just an object that can be indexed with numeric keys and has some extra methods. In fact you can assign pretty much any non primitive value to that type.

const a: {[k:string]: any} = [1,2,3]; // works
const b: {[k:string]: any} = {a: 123}; // works
const c: {[k:string]: any} = () => { console.log(123) }; // works
const d: {[k:string]: any} = () => new AnyClass(); // works

Playground

This works for the same reason you can do the following, because any is the one case where typescript always lets you cast a value to.

const a: any = true
const b: any = {}
const c: any = new AnyClass()

Playground

So you have a few options.

  1. Constrain your type, so that you aren't casting to any. If you know what possible values are on those properties, declare them.
interface MyObjectType { [k: string]: number | string }
const a: MyObjectType = [] // fails
const b: MyObjectType = {} // works

Playground

Or perhaps this is JSON? If so, any isn't the right type since you know it can't have some things (like class instances or functions).

interface Json {
  [key: string]: string | number | boolean | Json | Json[]
}

const a: Json = [] // type error
const b: Json = {} // works

Playground

  1. Or use the unknown type instead of any, which requires that you check the type at runtime before using the values.
interface MyObjectType { [k: string]: unknown }

const a: MyObjectType = [] // type error
const b: MyObjectType = { prop: 123 } // works

// b.prop type here is: unknown
b.prop.toUpperCase() // type error

if (typeof b.prop === 'string') {
  // a.prop type here is: string
  console.log(b.prop.toUpperCase()) // works
}

Playground

1 Comment

Excellent answer! I'm using your JSON approach and it works like a charm. Thanks!
6

type MapOnly = Record<keyof any, unknown>

3 Comments

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
FYI keyof any is the same as PropertyKey, i.e. string | number | symbol
Useful answer. It disallows null (which has a typeof of object in the runtime), but I think that's desirable in most cases and you could simply union it with null if you want.
4

Use this

type NonArrayObject = object & { [Symbol.iterator]?: never; }

Except your object is iterable, the above is safer in most cases. The below will work in some cases where your object will have no length property or has unknown property type.

type NonArrayObject = object & { length?: never; }
// or
type NonArrayObject = {[key: string]: unknown }

Playground

1 Comment

thanks, nice type. I can't believe we don't have a native ts helper for this basic type/use-case though.
3

This seems to do what you need:

type NonArrayObject = {
    [x: string]: any
    [y: number]: never
}

let p: NonArrayObject = {}             // fine
let q: NonArrayObject = { foo: "bar" } // fine
let r: NonArrayObject = []             // type error
let s: NonArrayObject = ["foo", 3]     // type error

Edit: The type error for empty arrays seems to be a local artefact. In the playground it only seems to prevent populated arrays. Maybe that helps a little :)

3 Comments

This is a really smart approach!
Yep, had to do this to remove empty arrays also: type NonArrayObject = { [k: string]: any, [y: number]: never } & [any, ...any] Not sure if there's a better solution or not
r is not an error (but it should be)
1

This is what I came up with, which works and has a somewhat helpful error message where others have a mostly unhelpful error message:

type NotArray = {
  length?: never
  [key: string]: any
} | string | bigint | number | boolean

const h: NotArray = [1] // errors
const h1: NotArray = ['1'] // errors
const h3: NotArray = [{h: 'h'}] // errors
const h4: NotArray = {h: ['h']}
const h5: NotArray = {h: 1}
const h2: NotArray = 1

Comments

0

Generic, that works for me:

type NoArray<T> = T extends Array<any> ? never : T;

const obj: NoArray<{length: number}> = {length: 123}; // works fine
const arr: NoArray<[{length: number}]> = [{length: 123}]; // TypeError

TS playground link

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.