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];
}
}