Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript type that matches any object but not arrays

Tags:

typescript

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
like image 757
Colin McDonnell Avatar asked Apr 10 '20 20:04

Colin McDonnell


People also ask

What is the TypeScript type for object?

In TypeScript, object is the type of all non-primitive values (primitive values are undefined , null , booleans, numbers, bigints, strings). With this type, we can't access any properties of a value.

What is type assertion in TypeScript?

In Typescript, Type assertion is a technique that informs the compiler about the type of a variable. Type assertion is similar to typecasting but it doesn't reconstruct code. You can use type assertion to specify a value's type and tell the compiler not to deduce it.

What is @types in TypeScript?

What is a type in TypeScript. In TypeScript, a type is a convenient way to refer to the different properties and functions that a value has. A value is anything that you can assign to a variable e.g., a number, a string, an array, an object, and a function. See the following value: 'Hello'

What is type alias in TypeScript?

In Typescript, Type aliases give a type a new name. They are similar to interfaces in that they can be used to name primitives and any other kinds that you'd have to define by hand otherwise. Aliasing doesn't truly create a new type; instead, it gives that type a new name.

What is the difference between JavaScript and typescript?

In JavaScript, the fundamental way that we group and pass around data is through objects. In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias.

What is an anonymous type in typescript?

In TypeScript, we represent those through object types. As we’ve seen, they can be anonymous: or a type alias. In all three examples above, we’ve written functions that take objects that contain the property name (which must be a string) and age (which must be a number ).

How to define an array of objects in typescript?

One of which is Array of Objects, in TypeScript, the user can define an array of objects by placing brackets after the interface. It can be named interface or an inline interface.

Why can't I assign arguments to string parameters in typescript?

Argument of type 'number' is not assignable to parameter of type 'string'. Argument of type 'number' is not assignable to parameter of type 'string'. Even if you don’t have type annotations on your parameters, TypeScript will still check that you passed the right number of arguments. You can also add return type annotations.


4 Answers

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

like image 81
Alex Wayne Avatar answered Sep 18 '22 12:09

Alex Wayne


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];
    }
}
like image 39
Kaspar Etter Avatar answered Sep 21 '22 12:09

Kaspar Etter


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 :)

like image 39
Zach Avatar answered Sep 18 '22 12:09

Zach


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
like image 36
bdombro Avatar answered Sep 22 '22 12:09

bdombro