现在 TypeScript 支持通过
in
运算符和 satisfies
关键字进行类型缩小,从 version 4.9 开始,我一直在寻找一种标准化的方法来以类型安全的方式编写 type 谓词函数 ,不使用任何类型断言。
这样,如果类型定义发生变化,TypeScript 将知道该类型的类型保护函数不再正确。
但是,我遇到了一些问题,看起来 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;
}
正如我在该示例中评论的那样,在使用
data
运算符缩小 in
的类型后,TypeScript 知道它具有 foo
属性。
但是,尽管使用
foo
运算符将其 string
属性的类型缩小为 typeof
,对象的整体类型仍然是 object & Record<"foo", unknown>
而不是 object & Record<"foo", string>
.
不过,我发现特别令人困惑的是,如果我去访问
data.foo
,那么 TypeScript 就可以很好地理解它的类型是 string
.
到目前为止我能想到的唯一解决方案是创建一个额外的类型谓词函数来告诉 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'; }
但是这个解决方案对我来说感觉不太好,因为它需要实用程序
propertyIsType
函数和实用程序类型谓词函数,例如 isString
对于需要测试的每种类型,甚至是原始类型。
我对
propertyIsType
的实现并没有替换类型的 Record<"foo", unknown>
部分,而是只添加了 Record<"foo", string>
,这也让人感觉有点混乱,但至少这不会引起任何问题。
是否有更好的解决方案来缩小像
Record<"foo", unknown>
到 Record<"foo", string>
这样的类型?
您可以简化您的检查
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;
}