带类型防护的缩小函数返回类型

时间:2018-09-26 16:16:35

标签: typescript narrowing

我有一个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吗?

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:1)

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,因为仅使用一个对象就可以将仅具有可选属性的接口与之匹配,而不会引发错误。

...老实说,这有点麻烦,我想知道这样做是否值得。