網站搜尋

typescript - 如何聲明一個函數來斷言兩個參數相等?


在 TypeScript 中,你可以將一個值傳遞給函數,並讓該函數斷言該值為 true,以便進行類型縮小。例如

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

然而,如果你想要有一個更高層級的函數來構造一個 v。例如

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

如果你調用 assertEqual,你不會得到任何類型縮小。一個權宜的解決方法是

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

但那需要像 assertEqual<'foo'>(v, 'foo') 這樣的調用,而這很容易出錯。

在此範例中,nameOf 和 nameOf3 都通過型別檢查,但 nameOf2 未通過:

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
}

答案#1

您可以像這樣從第二個參數推導出所斷言的通用類型信息:

TS 遊樂場

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

而且,正如 futur 所指出的,你可以使用 const 斷言來指示編譯器從參數推斷出字串字面值類型:

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

你可以創建一個斷言函數,專門用於推斷字串字面值:

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

答案#2

試試使用範本字面型別:

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
}

在 TS Playground 的完整範例。