我试图创建一个函数,它接受泛型类型并保留其内容的文字值,但不允许任何额外的键.

typescriptsatisfies关键字做了我所需要的—但从我所看到的来看,它只能在定义文字类型时使用,而不是作为函数参数使用.

这里有bare-bones example个我的意思:

type Foo = { a: number };

const foo = <T extends Foo>(x: T) => x;

foo({ a: 1, wrong: 2 }) // typechecks, but i don't want it to

foo({ a: 1, wrong: 2} satisfies Foo) // causes the type error i want

我试过各种各样的杂技,都没有用.然而,在发布了feature request on the Typescript repo之后,我被指出了Typescribe的一个新特性,它允许我得到一些工作(见comment).

我最终得到的解决方案是这个怪物:

type OptionalKeys<T extends Record<string, unknown>> = {
    // eslint-disable-next-line
    [P in keyof T]: {} extends Pick<T, P> ? P : never;
}[keyof T];

type RequiredKeys<T extends Record<string, unknown>> = {
    // eslint-disable-next-line
    [P in keyof T]: {} extends Pick<T, P> ? never : P;
}[keyof T];

type Satisfies<T, Base> =
    // recur if both the generic type and its base are records
    T extends Record<string, unknown>
        ? Base extends Record<string, unknown>
            // this check is to make sure i don't intersect with {}, allowing any keys
            ? (keyof T & RequiredKeys<Base> extends never
                  ? unknown
                  : {
                        [K in keyof T & RequiredKeys<Base>]: Satisfies<
                            T[K],
                            Base[K]
                        >;
                    }) &
                   // this check is to make sure i don't intersect with {}, allowing any keys
                  (keyof T & OptionalKeys<Base> extends never
                      ? unknown
                      : {
                            [K in keyof T & OptionalKeys<Base>]?: Satisfies<
                                T[K],
                                Base[K]
                            >;
                        })
            // if the generic type is a record but the base type isn't, something has gone wrong
            : never
        : T extends (infer TE)[]
          ? Base extends (infer BE)[]
              ? Satisfies<TE, BE>[]
               // if the generic type is an array but the base type isn't, something has gone wrong
              : never
          // the type is a scalar so no need to recur
          : T;

这里是一个ts playground that demonstrates this solution in action,通过引起以下示例的正确错误:

type Foo = { a: { b?: number }[] };

const foo = <T extends Foo>(x: Satisfies<T, Foo>) => x;

foo({ a: [{}] }); // typechecks
foo({ a: [{ b: 3 }] }); // typechecks
foo({ x: 2 }); // error
foo({ a: [{}], x: 2 }); // error
foo({ a: [{ b: 2, x: 3 }] }); // error
foo({ a: [{ x: 3 }] }); // error

任何关于如何清理或以其他方式改善它的建议将不胜感激!我对这件事已经超出了我的舒适区.

推荐答案

您正在寻找所谓的"精确类型"(术语来自Flow),其中多余的属性被认为是错误. 见microsoft/TypeScript#12936.TypeScript不支持这些;它的所有类型都是"不精确的",以允许 struct 类型化.在某些情况下,对象文字中有一个warns on excess properties的特性,但这更多的是一个短绒警告,实际上并不是类型系统的一部分.

你可以用generics来模拟精确的类型,通过拧T extends Exactly<T, U>这样的东西,只有当T是"精确"U时才能工作,这意味着它没有任何额外的属性.您似乎关心这个recursively,所以我们可以编写一个DeepExactly<T, U>,其中T的所有类对象属性都被"精确地"对照U的属性进行判断.

这里有一种方式来写这个:

type DeepExactly<T, U> =
    T extends object ? U extends object ?
    { [K in keyof T]: K extends keyof U ? DeepExactly<T[K], U[K]> : never } :
    never : U

我们基本上是用mapped types把多余的属性转换成never,这很可能不兼容. 让我们来测试一下:

type Foo = { a: { b?: number }[] };
const foo = <T extends Foo>(x: T extends DeepExactly<T, Foo> ? T : DeepExactly<T, Foo>) => x;

foo({ a: [{}] }); // typechecks
foo({ a: [{ b: 3 }] }); // typechecks
foo({ x: 2 }); // error
foo({ a: [{}], x: 2 }); // error
foo({ a: [{ b: 2, x: 3 }] }); // error
foo({ a: [{ x: 3 }] }); // error

看起来不错!

请注意,这并不是万无一失的.TypeScript真的不能建模精确的类型,所以任何东西都不能阻止这一点,即使在原则上也是如此:

const x = { a: [{ b: 3, e: "haha" }], b: "hello", c: "whoopsiedoodle" };
const y: Foo = x;
foo(y) // okay

为了防止这种情况,你需要一个Exact<Foo>DeepExact<Foo>的类型,这将从字面上禁止多余的属性.这就是为什么Microsoft/TypeScript#12936仍然开放.

Playground link to code

Typescript相关问答推荐

TS 2339:属性切片不存在于类型DeliverableSignal产品[]上>'

TypScript在对象上强制执行值类型时推断对象文字类型?

使用React处理Websocket数据

为什么我的Set-Cookie在我执行下一次请求后被擦除

Angular 错误NG0303在Angular 项目中使用app.Component.html中的NGFor

STypescript 件的标引签名与功能签名的区别

使用打字Angular 中的通用数据创建状态管理

为什么不能使用$EVENT接收字符串数组?

分解对象时出现打字错误

Angular material 无法显示通过API获取的数据

在类型脚本中将泛型字符串数组转换为字符串字母并集

避免混淆两种不同的ID类型

类型与泛型抽象类中的类型不可比较

打字错误TS2305:模块常量没有导出的成员

垫子工具栏不起作用的Angular 布线

基于闭包类型缩小泛型类型脚本函数的范围

基于区分的联合键的回调参数缩小

可以将JS文件放在tsconfig';s includes/files属性?或者,我如何让tsc判断TS项目中的JS文件?

TypeScript中的这些条件类型映射方法之间有什么区别?

如何创建允许使用带有联合类型参数的Array.from()的TS函数?