typescript 编写没有类型Assert的类型 predicate 函数

toiithl6  于 2023-05-08  发布在  TypeScript
关注(0)|答案(1)|浏览(123)

既然TypeScript支持通过in运算符和satisfies关键字进行类型窄化,从version 4.9开始,我一直在寻找一种标准化的方法来以类型安全的方式编写类型 predicate 函数,而不使用任何类型Assert。
这样,如果类型定义更改,TypeScript将知道该类型的typeguard函数不再正确。
然而,我遇到了一些问题,看起来TypeScript正确地确定了每个对象属性的类型,但没有正确地缩小对象本身的类型。
下面是一个最小可重复的示例:

type MyType = {
    foo: string;
};

function isMyType(data: unknown): data is MyType {
    if (!(
        typeof data === 'object' &&
        data !== null
    )) {
        return false;
    }

    if (!(
        'foo' in data &&
        typeof data.foo === 'string'
    )) {
        return false;
    }

    data.foo; // <- Typed correctly as `string`
    data; // <- Typed incorrectly as `object & Record<"foo", unknown>`

    /* Error: 
    Type 'object & Record<"foo", unknown>' does not satisfy the expected type 'MyType'.
        Types of property 'foo' are incompatible.
            Type 'unknown' is not assignable to type 'string'. */
    data satisfies MyType;

    return true;
}

TypeScript Playground
正如我在那个例子中所评论的,在使用in运算符缩小data的类型之后,TypeScript理解它具有foo属性。
然而,尽管foo属性的类型已经使用typeof操作符缩小到string,但对象的总体类型仍然是object & Record<"foo", unknown>而不是object & Record<"foo", string>
不过,我发现特别令人困惑的是,如果我访问data.foo,那么TypeScript完全理解其类型是string
到目前为止,我能想到的唯一解决方案是创建一个额外的类型 predicate 函数来告诉TypeScript如何缩小data的属性类型,如下所示:

type MyType = {
    foo: string;
};

function isMyType(data: unknown): data is MyType {
    if (!(
        typeof data === 'object' &&
        data !== null
    )) {
        return false;
    }

    if (!(
        'foo' in data &&
        propertyIsType(data, 'foo', isString)
    )) {
        return false;
    }

    data.foo; // <- Typed correctly as `string`
    data; // <- Typed as `object & Record<"foo", unknown>` & Record<"foo", string>

    // No error
    data satisfies MyType;

    return true;
}

/**
 * Narrow the type of an object based on a test on a particular property's type
 */
function propertyIsType<
    R, PropName extends string | number | symbol, T
>(
    data: R & Record<PropName, unknown>,
    propName: PropName,
    predicate: (prop: unknown) => prop is T
): data is R & Record<PropName, T> {
    return predicate(data[propName]);
}

/**
 * Utility type predicate for the `string` type
 */
function isString(data: unknown): data is string { return typeof data === 'string'; }

TypeScript Playground
但是这个解决方案对我来说并不好,因为它需要一个实用程序propertyIsType函数和实用程序类型 predicate 函数,如isString,用于每个需要测试的类型,甚至是基本类型。
我的propertyIsType实现没有替换类型的Record<"foo", unknown>部分,而是只添加了Record<"foo", string>,这也让人感觉有点混乱,但至少这没有引起任何问题。
有没有更好的解决方案来缩小Record<"foo", unknown>Record<"foo", string>这样的类型?

k2fxgqgv

k2fxgqgv1#

您可以将检查简化为

type MyType = {
    foo: string;
};

interface typeofMap {
    'number': number
    'string': string
}

function propIs<K extends string, T extends keyof typeofMap>(
    o: unknown, k: K, t: T
): o is { [P in K]: typeofMap[T] } {
    return typeof o === 'object' && o !== null && k in o && typeof (o as Record<K, any>)[k] === t
}

function isMyType(data: unknown): data is MyType {
    if (!propIs(data, 'foo', 'string')) {
        return false;
    }

    data.foo; // <- Typed correctly as `string`
    data; // <- Typed as `(parameter) data: { foo: string; }

    // No error
    data satisfies MyType;

    return true;
}

相关问题