Home > Net >  How do you declare a function asserts two arguments are equal?
How do you declare a function asserts two arguments are equal?

Time:03-01

In typescript you can pass a value to a function and have that function assert the value is true for the sake of type narrowing. eg

function assertTrue(v: unknown, message: string): asserts v {
  if (!v) {
    throw new MySpecialError(message)
  }
}

However, what if you wanted to have a higher level function that constructed a v. eg

function assertEqual(v: unknown, expected: unknown) {
  assertTrue(v === expected, `expected ${v} to be ${expected}`)
}

If you call assertEqual you do not get any type narrowing. A hack solution is

function assertEqual<T>(v: unknown, expected: T): asserts v is T {
  assertTrue(v === expected, `expected ${v} to be ${expected}`)
}

but that requires calls like assertEqual<'foo'>(v, 'foo') which is easy to get wrong.

In this example, nameOf and nameOf3 both type check but nameOf2 does not:

export type Staged =
  | {
      stage: 'pre-named';
    }
  | {
      stage: 'named';
      name: string;
    };

class MySpecialError extends Error {}

function assertTrue(v: unknown, message: string): asserts v {
  if (!v) {
    throw new MySpecialError(message)
  }
}

function nameOf(staged: Staged) {
    assertTrue(staged.stage === 'named', 'must be named')
    return staged.name
}

function assertEqual<T>(v: unknown, expected: T): asserts v is T {
  assertTrue(v === expected, `expected ${v} to be ${expected}`)
}

function nameOf2(staged: Staged) {
    assertEqual(staged.stage, 'named')
    return staged.name
}

function nameOf3(staged: Staged) {
    assertEqual<'named'>(staged.stage, 'named')
    return staged.name
}

CodePudding user response:

You can derive the asserted generic type information from the second parameter like this:

TS Playground

function assertStrictEquals <T>(actual: unknown, expected: T): asserts actual is T {
  if (actual !== expected) throw new Error('Values not equal');
}

const expected = { msg: 'hello world' };
const actual: unknown = expected; // unknown

assertStrictEquals(actual, expected);

actual; // { msg: string; }

And, as pointed out by futur, you can use a const assertion to instruct the compiler to infer a string literal type from an argument:

function nameOf2 (staged: Staged): string {
  assertStrictEquals(staged.stage, 'named' as const);
  return staged.name;
}

You can create an assertion function exclusively for inferring string literal values:

function assertIsString <T extends string>(actual: string, expected: T): asserts actual is T {
  assert(actual === expected, 'Strings not equal');
}

CodePudding user response:

Try using Template Literal Types:

type AssertType<T> = `${any & T}`;

function assertEqual<K, T extends (unknown extends T ? AssertType<K> : K)>(v: unknown, expected: T): asserts v is T {
  assertTrue(v === expected, `expected ${v} to be ${expected}`)
}

function nameOf(staged: Staged) {
    assertTrue(staged.stage === 'named', 'must be named')
    return staged.name
}

function nameOf2(staged: Staged) {
    assertEqual(staged.stage, 'named')
    return staged.name
}

function nameOf3(staged: Staged) {
    assertEqual<string, 'named'>(staged.stage, 'named')
    return staged.name
}

Full example at TS Playground.

  • Related