Can a constructor ever return a primitive?
Asked Answered
C

2

7

I'm asking this question because I've noticed that TypeScript allows declaring constructors that return primitive types, e.g.:

type Constructor1 = new () => string; // Primitive string

as opposed to

type Constructor2 = new () => String; // String object

This made me wonder if JavaScript actually permits creating a function that returns a primitive value when invoked with new semantics, i.e. a value that passes the primitiveness test:

function isPrimitive(value) {
    return value !== Object(value);
}

Needless to say, I could not find any example of a constructor invocation that produces a primitive value, so I imagine this must be just another weirdness of the TypeScript type model. Or does a primitive constructor really exist?


For reference, this is what I have tried out.

Predefined constructors

The predefined constructors Number, Boolean, String, etc. all produce an object when invoked with new, although they return a primitive value when called as regular functions. i.e.

isPrimitive(new Number()) // false

isPrimitive(Number())     // true

function isPrimitive(value) {
    return value !== Object(value);
}

console.log(isPrimitive(new Number()));
console.log(isPrimitive(Number()));

return in constructor

A return statement overrides the instance of this in the constructor, but only if the return value is an object:

const OBJECT = { foo: "bar" };
const PRIMITIVE = "baz";

function TheObject() {
    return OBJECT;
}

function ThePrimitive() {
    return PRIMITIVE;
}

console.log(isPrimitive(new TheObject()));    // prints false
console.log(isPrimitive(new ThePrimitive())); // prints false

function isPrimitive(value) {
    return value !== Object(value);
}

const OBJECT = { foo: "bar" };
const PRIMITIVE = "baz";

function TheObject() {
    return OBJECT;
}

function ThePrimitive() {
    return PRIMITIVE;
}

console.log(isPrimitive(new TheObject()));    // prints false
console.log(isPrimitive(new ThePrimitive())); // prints false

construct trap

A proxy can provide a construct trap to handle invocations to a function with new syntax. Whatever object the trap returns will be also returned by the constructor invocation. But, if a trap returns a primitive value other than undefined, a TypeError occurs.

const FooConstructor = new Proxy(
    class { },
    { construct: () => 'foo' }
);

new FooConstructor(); // throws TypeError: proxy [[Construct]] must return an object

function isPrimitive(value) {
    return value !== Object(value);
}

const FooConstructor = new Proxy(
    class { },
    { construct: () => 'foo' }
);

new FooConstructor();

More ideas?

Cementum answered 2/12, 2021 at 7:34 Comment(4)
Do you have a practical concern that you're trying to serve? Generally, one would simply do this with a factory method.Devol
@RobertHarvey I was actually thinking just today that if an arbitrary constructor could return, say, null, I'd have to add some extra checks in one of my libraries to handle that situation. But no, my question is more about general curiosity.Cementum
Constructors can return whatever they want (except null IIRC). But as soon as you start manipulating a primitive it is converted to its associated object type. E.g., foo = 'foo' is a primitive string--but you can call methods on it, foo.toUpperCase(), because of the conversion. geeksforgeeks.org/… may help.Gerardgerardo
stackoverflow.com/questions/3350215Devol
B
5

Can a constructor ever return a primitive?

The ECMAScript specification defines a constructor as:

...an object that supports the [[Construct]] internal method.

Although exotic objects have some liberty in implementing internal methods, the specification states at 6.1.7.3 Invariants of the Essential Internal Methods:

The Internal Methods of Objects of an ECMAScript engine must conform to the list of invariants specified below. Ordinary ECMAScript Objects as well as all standard exotic objects in this specification maintain these invariants. ECMAScript Proxy objects maintain these invariants by means of runtime checks on the result of traps invoked on the [[ProxyHandler]] object.

Any implementation provided exotic objects must also maintain these invariants for those objects. Violation of these invariants may cause ECMAScript code to have unpredictable behaviour and create security issues. However, violation of these invariants must never compromise the memory safety of an implementation.

An implementation must not allow these invariants to be circumvented in any manner such as by providing alternative interfaces that implement the functionality of the essential internal methods without enforcing their invariants.

[...]

The value returned by any internal method must be a Completion Record with either:

  • [[Type]] = normal, [[Target]] = empty, and [[Value]] = a value of the "normal return type" shown below for that internal method, or
  • [[Type]] = throw, [[Target]] = empty, and [[Value]] = any ECMAScript language value.

[...]

[[Construct]] ( )

  • The normal return type is Object.

[...]

So in conclusion, a compliant ECMAScript implementation does not allow the return value of the [[Construct]] internal method to be a primitive.

Take note that "normal return type" has a specific meaning here, which is also introduced in the quote above. "Normal" here refers to the case where no error was thrown.


You also phrased your question like this:

This made me wonder if JavaScript actually permits creating a function that returns a primitive value when invoked with new semantics

At 13.3.5 The new Operator, the specification stipulates that the Construct procedure is executed (if all checks pass):

  1. Return ? Construct(constructor, argList).

And the procedure at 7.3.15 Construct ( F [ , argumentsList [ , newTarget ] ] ) in turn specifies:

  1. Return ? F.[[Construct]](argumentsList, newTarget).

So the new operator will lead to the execution of the [[Construct]] internal method, and so the above applies.

Beggary answered 5/12, 2021 at 21:29 Comment(0)
E
2

A constructor function can return anything it wants, including primitives. It most often does not return anything, resulting in the primitive undefined.

However, any invocation of a constructor with new will follow the well-established rules and always return an object. This is declared in the specification for the internal [[construct]] method, which has the signature (a List of any, Object) → Object and is also specified with the clear invariant

[[Construct]]()

  • The normal return type is Object.
  • The target must also have a [[Call]] internal method.

This invariant applies to ordinary objects as well as to exotic ones, be they native or host-defined.

So yes, the TypeScript behaviour you described should be forbidden, a new signature cannot result in a primitive value. new () => string might as well be a compile-time error. Notice however that you won't be able to construct a value that is assignable to this type without resorting to any typecasts, so you might as well consider it equivalent to never.

Educated answered 5/12, 2021 at 21:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.