typescripttestingtdddenotype-systems

Can assert a variable to be an instance of a String in Deno


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


Solution

  • 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");
      }
    }