0

Can't seem to be able to assert instances of string in Deno:

import {
  assertInstanceOf
} from "https://deno.land/std@0.174.0/testing/asserts.ts";

assertInstanceOf( "foo", string );

Throws:

error: TS2693 [ERROR]: 'string' only refers to a type, but is being used as a value here.
assertInstanceOf( "foo", string );
                         ~~~~~~
    at file:///home/jmerelo/Code/my-stackoverflow-examples/js/string-assert.ts:6:26

Fair enough, let's try this

assertInstanceOf( "foo", String );

Now I'm confused:

Uncaught error from ./string-assert.ts FAILED

 ERRORS 

./string-assert.ts (uncaught error)
error: AssertionError: Expected object to be an instance of "String" but was "string".

Any idea of what would be the correct type here?

There's clearly a workaround here, to use typeof. But I would like to know what's the solution to this Catch-22

jjmerelo
  • 22,578
  • 8
  • 40
  • 86
  • 1
    Shouldn't it be `"string"` the *string* with content "string"? The `string` identifier all lowercase shouldn't refer to anything at runtime. Unless there is something in the setup that makes the ambient context available at runtime? `String` in uppercase would refer to the string *constructor*. And primitive strings aren't instances of it: `"hello" instanceof String` is `false`. – VLAZ Jan 31 '23 at 14:29
  • 1
    Checking [the documentation](https://deno.land/manual@v1.30.0/basics/testing/assertions#instance-types) it doesn't say you can assert primitives. It very explicitly states "*To check if an object is an instance of a specific constructor*" which, again, primitives aren't instances of a constructor. Clearly doesn't do TS type-level checking at runtime. – VLAZ Jan 31 '23 at 14:31
  • @VLAZ so I guess it's the workaround, right? – jjmerelo Jan 31 '23 at 16:17
  • The documentation also shows you can create your own asserters. So you can probably just make one for primitive types. – VLAZ Jan 31 '23 at 16:17
  • Related: [Why does instanceof return false for some literals?](https://stackoverflow.com/q/203739) – VLAZ Jan 31 '23 at 18:26

1 Answers1

3

There's clearly a workaround here, to use typeof. But I would like to know what's the solution to this Catch-22

It's not a Catch-22, but a false premise. In JavaScript: while primitives do appear object-like in some aspects, they are not objects (see JavaScript data types and data structures) — therefore they are not useful operands for use with the instanceof operator because the evaluation will always be false (see spec):

instanceof

The instanceof operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object.

For strings, this is explained further on MDN's String article in the section String primitives and String objects.

Below is a code example demonstrating how to discriminate and assert whether a value is a string literal or an object instance of String using Deno's standard testing library and user-defined type guard functions.

TS Playground

module.ts:

import { assert } from "https://deno.land/std@0.175.0/testing/asserts.ts";

function isStringLiteral(actual: unknown): actual is string {
  return typeof actual === "string";
}

function isStringInstance(actual: unknown): actual is String {
  return typeof actual === "object" && actual instanceof String;
}

function isAnyString(actual: unknown): actual is string | String {
  return isStringLiteral(actual) || isStringInstance(actual);
}

const testCases: [name: string, value: unknown][] = [
  ["string literal", "foo"],
  ["string instance", new String("foo")],
  ["number literal", 42],
  ["number instance", new Number(42)],
];

console.log("Testing for string types...");

for (const [name, value] of testCases) {
  try {
    assert(isAnyString(value));
    console.log("✅", name);
  } catch {
    console.error("❌", name);
    continue;
  }

  try {
    assert(isStringLiteral(value));
    console.log("type:", "literal");
  } catch {
    assert(isStringInstance(value));
    console.log("type:", "instance");
  }
}

Output:

% deno --version
deno 1.30.0 (release, x86_64-apple-darwin)
v8 10.9.194.5
typescript 4.9.4

% deno check module.ts

% echo $?
0

% deno run module.ts
Testing for string types...
✅ string literal
type: literal
✅ string instance
type: instance
❌ number literal
❌ number instance

Compiled JavaScript with imports inlined:

// import { assert } from "https://deno.land/std@0.175.0/testing/asserts.ts";

// ---> Begin inlined imports

// https://deno.land/std@0.175.0/testing/asserts.ts?source#L19
class AssertionError extends Error {
  name = "AssertionError";
  constructor(message) {
    super(message);
  }
}

// https://deno.land/std@0.175.0/testing/asserts.ts?source#L138
/** Make an assertion, error will be thrown if `expr` does not have truthy value. */
function assert(expr, msg = "") {
  if (!expr) {
    throw new AssertionError(msg);
  }
}

// <--- End inlined imports

function isStringLiteral(actual) {
  return typeof actual === "string";
}
function isStringInstance(actual) {
  return typeof actual === "object" && actual instanceof String;
}
function isAnyString(actual) {
  return isStringLiteral(actual) || isStringInstance(actual);
}
const testCases = [["string literal", "foo"], ["string instance", new String("foo")], ["number literal", 42], ["number instance", new Number(42)]];
console.log("Testing for string types...");
for (const [name, value] of testCases) {
  try {
    assert(isAnyString(value));
    console.log("✅", name);
  } catch {
    console.error("❌", name);
    continue;
  }
  try {
    assert(isStringLiteral(value));
    console.log("type:", "literal");
  } catch {
    assert(isStringInstance(value));
    console.log("type:", "instance");
  }
}
jsejcksn
  • 27,667
  • 4
  • 38
  • 62