【问题标题】:TypeScript does not narrow non-literal typesTypeScript 不会缩小非文字类型
【发布时间】:2021-10-25 19:52:37
【问题描述】:

假设我有一个简单的解析器:

const str = "String to test";
let pos = 0;
function eat(
  ...args: (
    | [RegExp, (result: RegExpExecArray) => boolean]
    | [string, (result: string) => boolean]
    | [string[], (result: number) => boolean]
  )[]
) {
  const oldPos = pos;
  for (const [test, callback] of args) {
    if (test instanceof RegExp) {
      test.lastIndex = pos;
      const match = test.exec(str);
      if (match) {
        pos = test.lastIndex;
        if (callback(match)) return true;
        break;
      }
    } else if (typeof test === "string") {
      if (str.substr(pos, test.length) === test) {
        pos += test.length;
        if (callback(test)) return true;
        break;
      }
    } else {
      const temp = test.findIndex(item => str.substr(pos, item.length) === item);
      if (temp != -1) {
        pos += test[temp].length;
        if (callback(temp)) return true;
        break;
      }
    }
  }
  pos = oldPos;
  return false;
}
if (eat(
  [/string/iy, result /* RegExpExecArray */ => {
    // ...
    return true;
  }],
  [["foo", "bar"], result /* number */ => {
    // ...
    return true;
  }]
)) console.log("Matched");

TypeScript Playground

我不知道如何正确定义 args 的类型以使编译器满意。


我知道我可以只区分matchtesttempresults 传递给callbackas never,但这并不能推断出result 的类型当方法本身被调用时。

另一种方法是在之前添加一个字符串标记,如下所示:

function example(arg: ["string", string, number] | ["boolean", boolean, bigint]) {
    if (arg[0] === "string") {
        Math.floor(arg[2]); // compiler happy
    }
}

但是当调用时它会很冗长。 (在我的情况下似乎也不起作用。)

我也尝试过泛型和函数重载,但要么不起作用,要么不适合我的需要(我在调用函数时混合了 3 种类型的参数数组)。


有什么想法吗?我是否应该将此问题发布到 GitHub 上的 issues page 以防出现错误?

Here 是一个 TypeScript Playground,在此问题上进行了更多实验。)

【问题讨论】:

  • 我相信这个问题已经存在。我认为你应该使用像这里tsplay.dev/NBPnzW 这样的类型保护。

标签: typescript


【解决方案1】:

您可以构建一个类型保护,在一些额外的帮助下,可以使您的代码类型安全:

const str = 'String to test';
let pos = 0;

type TCallback<TTest, TParam> = [TTest, (result: TParam) => boolean];

function isCallback<TTest, TParam>(
    callback: TCallback<any, any>,
    validate: (test: TTest) => boolean
): callback is TCallback<TTest, TParam> {
    return validate(callback[0]);
}

function eat(
    ...args: (
        | TCallback<RegExp, RegExpExecArray>
        | TCallback<string, string>
        | TCallback<string[], number>
    )[]
) {
    const oldPos = pos;
    for (const arg of args) {
        if (isCallback<RegExp, RegExpExecArray>(arg, (test) => test instanceof RegExp)) {
            const [test, callback] = arg;
            test.lastIndex = pos;
            const match = test.exec(str);
            if (match) {
                pos = test.lastIndex;
                if (callback(match)) return true;
                break;
            }
        } else if (isCallback<string, string>(arg, (test) => typeof test === 'string')) {
            const [test, callback] = arg;
            if (str.substr(pos, test.length) === test) {
                pos += test.length;
                if (callback(test)) return true;
                break;
            }
        } else if (isCallback<string[], string>(arg, (test) => test instanceof Array)) {
            const [test, callback] = arg;
            const temp = test.findIndex((item) => str.substr(pos, item.length) === item);
            if (temp != -1) {
                pos += test[temp].length;
                if (callback(temp)) return true;
                break;
            }
        }
    }
    pos = oldPos;
    return false;
}

if (eat(
    [/string/iy,
    (result: RegExpExecArray) => {
        // ...
        return true;
    }],
    [['foo', 'bar'],
    (result: number) => {
        // ...
        return true;
    }]
))
    console.log('Matched');

请注意,我将参数的类型传递给 isCallback 那里,这样,你告诉 TypeScript 假设返回类型是你刚刚确认的。

我还提取了testcallback 变量in 类型保护条件,因为这是我们唯一可以确定输入哪个选项的地方。

【讨论】:

    猜你喜欢
    • 2020-03-11
    • 2020-07-22
    • 2021-01-19
    • 1970-01-01
    • 2017-08-12
    • 1970-01-01
    • 2021-04-23
    • 1970-01-01
    • 2021-12-27
    相关资源
    最近更新 更多