使用类型保护缩小函数返回类型

Posted

技术标签:

【中文标题】使用类型保护缩小函数返回类型【英文标题】:Narrowing function return type with type guards 【发布时间】:2019-03-02 11:51:40 【问题描述】:

我有一个 TypeScript 函数,它解析一些 JSON 并通过类型保护运行它以确保数据有效,以便编译时代码的其余部分知道它正在处理一个实际遵守预期接口的对象.

但是,我很难让 TypeScript 强制执行类型保护已运行。显然JSON.parse 返回any,它可以分配给任何其他类型,因此即使我指定了非any 返回类型,也会进行检查。

const validPerson = `"firstName": "John", "lastName": "Doe"`;
const invalidPerson = `"foo": 123`;

interface Person 
    firstName: string;
    lastName: string;


interface PersonGetter 
    (json: string): Person | undefined;


function isPerson(o: any): o is Person 
    return typeof o.firstName === "string" && typeof o.lastName === "string";


// BAD: Type checks, but it's overly permissive. `JSON.parse` could return anything.
const getPerson1: PersonGetter = (json) => 
    const o = JSON.parse(json);
    return o;


// GOOD (kinda): Requires type guard to pass.
// `unknown` requires TS 3, which is fine in general, but bad for me.
// Also, I feel like having to remember to case the return from `JSON.parse` is a responsibility the programmer shouldn't bear.
const getPerson2: PersonGetter = (json) => 
    const o: unknown = JSON.parse(json);
    if (isPerson(o)) 
        return o;
     else 
        return undefined;
    


// GOOD (kinda): Requires type guard to pass. Works in TS 2.8.
// Still, not great that I have to cast the return value from `JSON.parse`, but I could probably work around that.
type JSONPrimitive = string | number | boolean | null;
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
type JSONObject =  [member: string]: JSONValue ;
interface JSONArray extends Array<JSONValue> 

const getPerson3: PersonGetter = (json) => 
    const o: JSONValue = JSON.parse(json);
    if (isPerson(o)) 
        return o;
     else 
        return undefined;
    

TypeScript Playground link

选项 3 对我有用,但它使用 proposed JSON types that are still up for debate 并且仍然将责任放在实现者身上(他们可以很容易地根本不使用类型保护并且仍然认为他们遵守接口)。

看来JSON.parse 返回any 是我的问题的根源。我已经在strict 模式下运行,但它似乎仍然允许将显式键入为any 的内容扩展为函数的显式返回类型。

有没有另一种方法告诉 TypeScript 函数的返回值必须是它实现的接口中指定的返回类型,而不是any

【问题讨论】:

我不明白您在期待什么,并且对包含 getPersonWithUnknown 的目的感到困惑。 getPerson 和省略(不必要的)类型声明有什么问题?最后的 if 块将确保返回对象的类型是正确的。您是否不希望应用程序中的任何代码错误地返回错误的类型,这只是一个示例?如果是这样,在任何返回任何类型的函数之后使用某种类型保护,就像在第三个函数中所做的那样。 getPersonWithUnknown 只是一个正确抛出类型错误的函数示例(因为不能将unknown 分配给Person)。最终的getPerson 函数就是以此为基础的。我要问的是,如果我有一个应该返回 Personundefined 的函数的接口,我如何让 TypeScript 不说返回 any 的函数是可以接受的? 有点题外话:Person? 怎么不是 Playground 中的语法错误? any 的目的是跳过编译时测试,所以我不认为你想要什么是可能的。如果我错了,这是一个很好的问题并且实际上有办法。 @AndrewO 第三个可以与 any 一起使用,但 TypeScript 的类型语法中没有问号。在您的情况下,类型是 Person | undefined 并将被推断出来。 【参考方案1】:
const validPerson = `"firstName": "John", "lastName": "Doe"`;
const invalidPerson = `"foo": 123`;

interface Person 
    firstName: string;
    lastName: string;


function isPerson(o: any): o is Person 
    return typeof o.firstName === "string" && typeof o.lastName === "string";


function getPerson(json: string) 
    const o = JSON.parse(json);

    if (isPerson(o)) 
        return o;
     else 
        return undefined;
    

Minimal playground。确保勾选开启strictNullChecks

【讨论】:

该函数可以返回任何内容并且会进行类型检查。这就是我试图避免的。最终,我想要一个界面,上面写着“这里的函数接受输入参数并返回良好类型的结果”。基本上是一个服务或 DAO。作为一个最小的例子,我认为直接在函数上做会更好。如果我在示例中包含一个接口并让它实现它会有所帮助吗? @AndrewO 推断类型是Person | undefined,所以一切都按预期工作。如果您想对自己强制执行,请指定返回类型。 更新示例以包含接口定义。【参考方案2】:

JSON 在 lib.es5.d.ts 中声明。在您的项目中创建您自己的类型定义文件并声明一个新的全局 JSON 实例,该实例的定义从 parse() 返回一个虚拟类型,而不是 any

这样,您将不得不使用保护或强制转换结果,以避免在具有明确定义的返回类型的函数和方法中出现编译错误。

interface JSONStrict extends JSON 
    /**
      * Converts a javascript Object Notation (JSON) string into an object.
      * @param text A valid JSON string.
      * @param reviver A function that transforms the results. 
      * This function is called for each member of the object.
      * If a member contains nested objects, the nested objects are
      * transformed before the parent object is.
      */
    parse(text: string, reviver?: (key: any, value: any) => any):  _dummyProp?: void ;

// overide lib.es5 declaration of JSON
declare const JSON: JSONStrict;

/* ... */
function parseAndThrowCompilationError(): Person 
    var result = JSON.parse(' "x": 1');
    return result; 
    // Type ' _dummyProp?: void ' has no properties in common with type 'Person'

我在结果中添加了_dummyProp,因为仅使用对象将匹配仅具有可选属性的接口并且不会引发错误。

...老实说,这有点麻烦,我想知道付出的努力是否真的值得。

【讨论】:

以上是关于使用类型保护缩小函数返回类型的主要内容,如果未能解决你的问题,请参考以下文章

5种在TypeScript中使用的类型保护

有没有办法提供一个宏函数来返回不同类型的值,包括啥?

TS泛型类、泛型接口、泛型函数

TypeScript 学习笔记 — 类型推断和类型保护

是否可以为具有 1)返回类型 void、2)访问说明符私有或受保护的方法编写单元测试?

cpp(第十三章)