Typescript: 确切类型

创建于 2016-12-15  ·  171评论  ·  资料来源: microsoft/TypeScript

这是一个为精确类型启用语法的提议。 在 Flow (https://flowtype.org/docs/objects.html#exact-object-types) 中可以看到类似的功能,但我想建议将它作为用于类型文字而不是接口的功能。 我建议使用的特定语法是管道(它几乎反映了 Flow 实现,但它应该围绕类型语句),因为它与数学绝对语法一样熟悉。

interface User {
  username: string
  email: string
}

const user1: User = { username: 'x', email: 'y', foo: 'z' } //=> Currently errors when `foo` is unknown.
const user2: Exact<User> = { username: 'x', email: 'y', foo: 'z' } //=> Still errors with `foo` unknown.

// Primary use-case is when you're creating a new type from expressions and you'd like the
// language to support you in ensuring no new properties are accidentally being added.
// Especially useful when the assigned together types may come from other parts of the application 
// and the result may be stored somewhere where extra fields are not useful.

const user3: User = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Does not currently error.
const user4: Exact<User> = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Will error as `foo` is unknown.

如果用作参数或公开类型,此语法更改将是一项新功能,并且会影响正在编写的新定义文件。 此语法可以与其他更复杂的类型结合使用。

type Foo = Exact<X> | Exact<Y>

type Bar = Exact<{ username: string }>

function insertIntoDb (user: Exact<User>) {}

如果这是重复的,请提前道歉,我似乎找不到正确的关键字来查找此功能的任何重复项。

编辑:这篇文章已更新为使用https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -267272371 中提到的首选语法提议,其中包括使用更简单的语法和泛型类型来启用表达式。

Awaiting More Feedback Suggestion

最有用的评论

我们讨论了很长一段时间。 我将尝试总结讨论。

超额财产检查

精确类型只是检测额外属性的一种方式。 当我们最初实施多余属性检查 (EPC) 时,对精确类型的需求下降了很多。 EPC 可能是我们采取的最大的突破性改变,但它已经得到了回报; 当 EPC没有检测到多余的属性时,我们几乎立即就遇到了错误。

对于人们想要确切类型的大多数情况,我们更愿意通过使 EPC 更智能来解决这个问题。 这里的一个关键领域是当目标类型是联合类型时——我们只想把它作为一个错误修复(EPC应该在这里工作,但它只是还没有实现)。

全选类型

与 EPC 相关的是全可选类型(我称之为“弱”类型)的问题。 最有可能的是,所有弱类型都希望是精确的。 我们应该只实现弱类型检测(#7485 / #3842); 这里唯一的阻碍是交叉类型,它在实现中需要一些额外的复杂性。

谁的类型准确?

我们看到的关于精确类型的第一个主要问题是实际上不清楚哪些类型应该被标记为精确。

一方面,如果给定具有某个固定域之外的自有密钥的对象,那么您的函数实际上会抛出异常(或以其他方式做坏事)。 这些很少见(我无法从记忆中说出一个例子)。 中间有默默忽略的函数
未知属性(几乎全部)。 而在另一端,您有通常对所有属性进行操作的函数(例如Object.keys )。

显然,“如果给定额外数据将抛出”函数应该被标记为接受精确类型。 但是中间呢? 人们可能会不同意。 Point2D / Point3D就是一个很好的例子——你可以合理地说magnitude函数应该具有(p: exact Point2D) => number以防止传递Point3D . 但是为什么我不能将{ x: 3, y: 14, units: 'meters' }对象传递给该函数? 这就是 EPC 的用武之地——您想在肯定会丢弃它的位置检测“额外”的units属性,但实际上不会阻止涉及别名的调用。

违反假设/实例化问题

我们有一些基本原则,确切的类型会失效。 例如,假定类型T & U始终可分配给T ,但如果T是精确类型,则此操作失败。 这是有问题的,因为您可能有一些使用此T & U -> T原则的泛型函数,但调用该函数时使用T实例化的确切类型。 因此,我们无法发出这种声音(实例化时出错确实不好)-不一定是阻止程序,但是让通用函数比其手动实例化的版本更宽松会令人困惑!

还假设T始终可分配给T | U ,但如果U是精确类型,则如何应用此规则并不明显。 { s: "hello", n: 3 }分配给{ s: string } | Exact<{ n: number }> ? “是”似乎是错误的答案,因为无论谁查找n并发现看到s都不会高兴,但是“否”似乎也是错误的,因为我们违反了基本的T -> T | U规则。

杂项

function f<T extends Exact<{ n: number }>(p: T)是什么意思? :使困惑:

通常需要精确类型,而您真正想要的是“自动脱节”联合。 换句话说,您可能有一个可以接受{ type: "name", firstName: "bob", lastName: "bobson" }{ type: "age", years: 32 }但不想接受{ type: "age", years: 32, firstName: 'bob" }的 API,因为会发生一些不可预测的事情。 “正确”类型可以说是{ type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined }但很好的天哪,打字很烦人。 我们可能会考虑用糖来创建这样的类型。

总结:需要的用例

我们希望的诊断是,除了相对较少的真正封闭的 API 之外,这是一个XY 问题解决方案。 在可能的情况下,我们应该使用 EPC 来检测“坏”属性。 因此,如果您遇到问题并且您认为精确类型是正确的解决方案,请在此处描述原始问题,以便我们可以编写一个模式目录,看看是否有其他解决方案可以减少侵入性/混淆。

所有171条评论

我建议这里的语法是有争议的。 由于 TypeScript 现在允许联合类型的前导管道。

class B {}

type A = | number | 
B

由于自动分号插入,现在编译并等效于type A = number | B

我认为如果引入了确切的类型,这可能不是我所期望的。

不确定是否真实但仅供参考https://github.com/Microsoft/TypeScript/issues/7481

如果采用{| ... |}语法,我们可以建立在映射类型上,以便您可以编写

type Exact<T> = {|
    [P in keyof T]: P[T]
|}

然后你可以写Exact<User>

与 TypeScript 相比,这可能是我从 Flow 中错过的最后一件事。

Object.assign示例特别好。 我理解为什么 TypeScript 会像今天这样行事,但大多数时候我宁愿拥有确切的类型。

@HerringtonDarkholme谢谢。 我最初的问题提到了这一点,但我最后省略了它,因为无论如何有人会有更好的语法,事实证明他们这样做了😄

@DanielRosenwasser看起来更合理,谢谢!

@wallverb我不这么认为,尽管我也希望看到该功能存在😄

如果我想表达类型的联合,其中一些是精确的,而另一些不是,该怎么办? 建议的语法会使它容易出错且难以阅读,即使对间距给予额外的注意:

|Type1| | |Type2| | Type3 | |Type4| | Type5 | |Type6|

你能快速说出工会的哪些成员不准确吗?

没有仔细的间距?

|Type1|||Type2||Type3||Type4||Type5||Type6|

(答案: Type3 , Type5

@rotemdan看到上面的答案,有一个泛型类型Extact代替它是一个比我的更可靠的建议。 我认为这是首选方法。

还有一个问题是它在编辑器提示、预览弹出窗口和编译器消息中的外观。 类型别名目前只是“扁平化”为原始类型表达式。 别名不会被保留,因此难以理解的表达式仍会出现在编辑器中,除非采取一些特殊措施来抵消这种情况。

我发现很难相信这种语法会被像 Flow 这样的编程语言所接受,它确实具有与 Typescript 相同语法的联合。 对我来说,引入一种与现有语法根本冲突的有缺陷的语法,然后非常努力地“覆盖”它似乎并不明智。

一个有趣(有趣?)的替代方法是使用像only这样的修饰符。 我想几个月前我有一个提案草案,但我从未提交过:

function test(a: only string, b: only User) {};

那是我当时能找到的最好的语法。

_Edit_: just也可以吗?

function test(a: just string, b: just User) {};

_(编辑:现在我记得语法最初是用于名义类型的修饰符,但我想这并不重要。这两个概念足够接近,所以这些关键字也可以在这里工作)_

我想知道,也许可以引入两个关键字来描述两种略有不同的匹配类型:

  • just T (意思是:“完全T ”)用于精确的结构匹配,如此处所述。
  • only T (意思是:“唯一的T ”)用于名义匹配。

名义匹配可以被视为精确结构匹配的“更严格”版本。 这意味着不仅类型必须在结构上相同,值本身也必须与指定的完全相同的类型标识符相关联。 除了接口和类之外,这可能支持也可能不支持类型别名。

我个人不认为细微的差异会造成如此大的混乱,尽管我觉得应该由 Typescript 团队来决定像only这样的名义修饰符的概念是否适合他们。 我只是建议将此作为一种选择。

_(编辑:只是关于only与类一起使用时的注释:关于在引用基类时是否允许名义子类存在歧义 - 我猜这需要单独讨论。对于一个程度较低 - 可以考虑同样用于接口 - 虽然我目前不认为它会那么有用)_

这似乎有点像变相的减法类型。 这些问题可能是相关的: https : https://github.com/Microsoft/TypeScript/issues/7993

@ethanresnick你为什么相信?

这在我现在正在处理的代码库中非常有用。 如果这已经是语言的一部分,那么我今天就不会花时间追踪错误。

(也许是其他错误,但不是这个特定错误 😉)

我不喜欢受 Flow 启发的管道语法。 像exact接口后面的关键字这样的东西会更容易阅读。

exact interface Foo {}

@mohsen1我相信大多数人会在表达式位置中使用Exact泛型类型,所以它应该没有太大关系。 但是,我会担心这样的提议,因为您可能会过早地重载以前仅用于导出的 interface 关键字的左侧(与 JavaScript 值一致 - 例如export const foo = {} )。 它还表明该关键字可能也可用于类型(例如exact type Foo = {} ,现在它将是export exact interface Foo {} )。

使用{| |}语法extends将如何工作? 将interface Bar extends Foo {| |}是准确的,如果Foo不准确?

我认为exact关键字可以很容易地判断一个接口是否准确。 它也可以(应该?)为type

interface Foo {}
type Bar = exact Foo

对于通过数据库或网络调用数据库或软件开发工具包(如AWS SDK)的事情非常有帮助,这些软件使用具有所有可选属性的对象,因为附加数据被默默忽略并可能导致很难甚至很难找到错误:rose:

@mohsen1这个问题似乎与语法无关,因为使用关键字方法仍然存在相同的问题。 就我个人而言,我没有首选答案,并且必须按照现有的期望来回答它 - 但我最初的反应是Foo是否准确并不重要。

exact关键字的用法似乎模棱两可-您是说它可以像exact interface Foo {}type Foo = exact {}exact Foo | Bar是什么意思? 使用通用方法并使用现有模式意味着不需要重新发明或学习。 它只是interface Foo {||} (这是这里唯一的新事物),然后是type Foo = Exact<{}>Exact<Foo> | Bar

我们讨论了很长一段时间。 我将尝试总结讨论。

超额财产检查

精确类型只是检测额外属性的一种方式。 当我们最初实施多余属性检查 (EPC) 时,对精确类型的需求下降了很多。 EPC 可能是我们采取的最大的突破性改变,但它已经得到了回报; 当 EPC没有检测到多余的属性时,我们几乎立即就遇到了错误。

对于人们想要确切类型的大多数情况,我们更愿意通过使 EPC 更智能来解决这个问题。 这里的一个关键领域是当目标类型是联合类型时——我们只想把它作为一个错误修复(EPC应该在这里工作,但它只是还没有实现)。

全选类型

与 EPC 相关的是全可选类型(我称之为“弱”类型)的问题。 最有可能的是,所有弱类型都希望是精确的。 我们应该只实现弱类型检测(#7485 / #3842); 这里唯一的阻碍是交叉类型,它在实现中需要一些额外的复杂性。

谁的类型准确?

我们看到的关于精确类型的第一个主要问题是实际上不清楚哪些类型应该被标记为精确。

一方面,如果给定具有某个固定域之外的自有密钥的对象,那么您的函数实际上会抛出异常(或以其他方式做坏事)。 这些很少见(我无法从记忆中说出一个例子)。 中间有默默忽略的函数
未知属性(几乎全部)。 而在另一端,您有通常对所有属性进行操作的函数(例如Object.keys )。

显然,“如果给定额外数据将抛出”函数应该被标记为接受精确类型。 但是中间呢? 人们可能会不同意。 Point2D / Point3D就是一个很好的例子——你可以合理地说magnitude函数应该具有(p: exact Point2D) => number以防止传递Point3D . 但是为什么我不能将{ x: 3, y: 14, units: 'meters' }对象传递给该函数? 这就是 EPC 的用武之地——您想在肯定会丢弃它的位置检测“额外”的units属性,但实际上不会阻止涉及别名的调用。

违反假设/实例化问题

我们有一些基本原则,确切的类型会失效。 例如,假定类型T & U始终可分配给T ,但如果T是精确类型,则此操作失败。 这是有问题的,因为您可能有一些使用此T & U -> T原则的泛型函数,但调用该函数时使用T实例化的确切类型。 因此,我们无法发出这种声音(实例化时出错确实不好)-不一定是阻止程序,但是让通用函数比其手动实例化的版本更宽松会令人困惑!

还假设T始终可分配给T | U ,但如果U是精确类型,则如何应用此规则并不明显。 { s: "hello", n: 3 }分配给{ s: string } | Exact<{ n: number }> ? “是”似乎是错误的答案,因为无论谁查找n并发现看到s都不会高兴,但是“否”似乎也是错误的,因为我们违反了基本的T -> T | U规则。

杂项

function f<T extends Exact<{ n: number }>(p: T)是什么意思? :使困惑:

通常需要精确类型,而您真正想要的是“自动脱节”联合。 换句话说,您可能有一个可以接受{ type: "name", firstName: "bob", lastName: "bobson" }{ type: "age", years: 32 }但不想接受{ type: "age", years: 32, firstName: 'bob" }的 API,因为会发生一些不可预测的事情。 “正确”类型可以说是{ type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined }但很好的天哪,打字很烦人。 我们可能会考虑用糖来创建这样的类型。

总结:需要的用例

我们希望的诊断是,除了相对较少的真正封闭的 API 之外,这是一个XY 问题解决方案。 在可能的情况下,我们应该使用 EPC 来检测“坏”属性。 因此,如果您遇到问题并且您认为精确类型是正确的解决方案,请在此处描述原始问题,以便我们可以编写一个模式目录,看看是否有其他解决方案可以减少侵入性/混淆。

我看到人们对没有确切的对象类型感到惊讶的主要地方是Object.keysfor..in ——它们总是产生string类型而不是'a'|'b'用于输入{ a: any, b: any }

正如我在https://github.com/Microsoft/TypeScript/issues/14094 中提到的,并且您在 Miscellany 部分中描述了{first: string, last: string, fullName: string}符合{first: string; last: string} | {fullName: string}令人讨厌。

例如,假设类型 T & U 始终可分配给 T,但如果 T 是精确类型,则此操作失败

如果T是一个精确类型,那么大概T & Unever (或T === U )。 对?

或者UT的非精确子集

导致我提出这个建议的用例是 redux 减速器。

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

正如您在摘要中指出的那样,我的问题并不直接在于我需要精确的接口,而是需要展开运算符才能精确工作。 但是由于扩展运算符的行为是由 JS 给出的,我想到的唯一解决方案是定义准确的返回类型或接口。

我是否正确理解将T的值分配给Exact<T>将是一个错误?

interface Dog {
    name: string;
    isGoodBoy: boolean;
}
let a: Dog = { name: 'Waldo', isGoodBoy: true };
let b: Exact<Dog> = a;

在这个例子中,将Dog缩小到Exact<Dog>是不安全的,对吧?
考虑这个例子:

interface PossiblyFlyingDog extends Dog {
    canFly: boolean;
}
let c: PossiblyFlyingDog = { ...a, canFly: true };
let d: Dog = c; // this is okay
let e: Exact<Dog> = d; // but this is not

@leonadler是的,就是这个主意。 您只能将Exact<T>分配给Exact<T> 。 我的直接用例是验证函数将处理Exact类型(例如,将请求有效负载作为any并输出有效的Exact<T> )。 但是, Exact<T>可以分配给T

@nerumo

正如您在摘要中指出的那样,我的问题并不直接在于我需要精确的接口,而是需要展开运算符才能精确工作。 但是由于扩展运算符的行为是由 JS 给出的,我想到的唯一解决方案是定义准确的返回类型或接口。

我遇到了同样的问题并想出了这个解决方案,这对我来说是非常优雅的解决方法:)

export type State = {
  readonly counter: number,
  readonly baseCurrency: string,
};

// BAD
export function badReducer(state: State = initialState, action: Action): State {
  if (action.type === INCREASE_COUNTER) {
    return {
      ...state,
      counterTypoError: state.counter + 1, // OK
    }; // it's a bug! but the compiler will not find it 
  }
}

// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
  let partialState: Partial<State> | undefined;

  if (action.type === INCREASE_COUNTER) {
    partialState = {
      counterTypoError: state.counter + 1, // Error: Object literal may only specify known properties, and 'counterTypoError' does not exist in type 'Partial<State>'. 
    }; // now it's showing a typo error correctly 
  }
  if (action.type === CHANGE_BASE_CURRENCY) {
    partialState = { // Error: Types of property 'baseCurrency' are incompatible. Type 'number' is not assignable to type 'string'.
      baseCurrency: 5,
    }; // type errors also works fine 
  }

  return partialState != null ? { ...state, ...partialState } : state;
}

您可以在我的 redux 指南的这一部分找到更多信息:

请注意,这可以使用我的约束类型建议 (#13257) 在用户空间中解决:

type Exact<T> = [
    case U in U extends T && T extends U: T,
];

编辑:更新了与​​提案相关的语法

@piotrwitek谢谢,Partial 技巧完美运行,并且已经在我的代码库中发现了一个错误 ;) 这值得小样板代码。 但我仍然同意@isiahmeadows 的观点会更好

@piotrwitek使用 Partial _almost_ 解决了我的问题,但它仍然允许属性变为未定义,即使 State 接口不是它们(我假设是strictNullChecks)。

我最终得到了一些稍微复杂的东西来保留接口类型:

export function updateWithPartial<S extends object>(current: S, update: Partial<S>): S {
    return Object.assign({}, current, update);
}

export function updateWith<S extends object, K extends keyof S>(current: S, update: {[key in K]: S[key]}): S {
    return Object.assign({}, current, update);
}

interface I {
    foo: string;
    bar: string;
}

const f: I = {foo: "a", bar: "b"}
updateWithPartial(f, {"foo": undefined}).foo.replace("a", "x"); // Compiles, but fails at runtime
updateWith(f, {foo: undefined}).foo.replace("a", "x"); // Does not compile
updateWith(f, {foo: "c"}).foo.replace("a", "x"); // Compiles and works

@asmundg是正确的,解决方案将接受未定义,但从我的角度来看,这是可以接受的,因为在我的解决方案中,我仅使用具有所需参数的动作创建者作为有效载荷,这将确保没有未定义的值分配给不可为空的属性。
实际上,我在生产中使用这个解决方案已经有一段时间了,这个问题从未发生过,但让我知道你的担忧。

export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';

export const actionCreators = {
  changeBaseCurrency: (payload: string) => ({
    type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
  }),
}

store.dispatch(actionCreators.changeBaseCurrency()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.changeBaseCurrency(undefined)); // Argument of type 'undefined' is not assignable to parameter of type 'string'.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }

演示- 在选项中启用 strictNullChecks

如果需要,您也可以创建可为空的有效负载,您可以在我的指南中阅读更多内容: https :

Rest Types被合并时,这个特性可以很容易地成为它们的语法糖。

提议

类型相等逻辑应该是严格的——只有具有相同属性的类型或具有可以以父类型具有相同属性的方式实例化的其余属性的类型才被认为是匹配的。 为了保持向后兼容性,除非一个类型已经存在,否则所有类型都会添加一个合成剩余类型。 还添加了一个新标志--strictTypes ,它禁止添加合成其余参数。

--strictTypes下的等式:

type A = { x: number, y: string };
type B = { x: number, y: string, ...restB: <T>T };
type C = { x: number, y: string, z: boolean, ...restC: <T>T };

declare const a: A;
declare const b: B;
declare const c: C;

a = b; // Error, type B has extra property: "restB"
a = c; // Error, type C has extra properties: "z", "restC"
b = a; // OK, restB inferred as {}
b = c; // OK, restB inferred as { z: boolean, ...restC: <T>T }

c = a; // Error, type A is missing property: "z"
       // restC inferred as {}

c = b; // Error, type B is missing property: "z"
       // restC inferred as restB 

如果--strictTypes未打开,则...rest: <T>T属性会自动添加到A 。 这样行a = b;a = c;将不再是错误,就像后面两行变量b的情况一样。

关于违反假设的一句话

假设类型 T & U 始终可分配给 T,但如果 T 是精确类型,则此操作失败。

是的, &允许虚假逻辑,但string & number的情况也是如此。 stringnumber都是不同的刚性类型,不能相交,但类型系统允许。 精确类型也是刚性的,所以不一致仍然是一致的。 问题在于&运算符 - 它不健全。

是 { s: "hello", n: 3 } 可分配给 { s: string } | 精确<{ n: number }>。

这可以翻译为:

type Test = { s: string, ...rest: <T>T } | { n: number }
const x: Test = { s: "hello", n: 3 }; // OK, s: string; rest inferred as { n: number }

所以答案应该是“是”。 将精确类型与非精确类型联合是不安全的,因为非精确类型包含所有精确类型,除非存在鉴别器属性。

回复:上面@RyanCavanaugh评论中的f<T extends Exact<{ n: number }>(p: T)函数,在我的一个库中,我非常想实现以下功能:

const checkType = <T>() => <U extends Exact<T>>(value: U) => value;

即一个函数返回其参数的类型完全相同,但同时还要检查它的类型是否与另一个(T)的类型完全相同。

这是一个有点人为的例子,我尝试了三个失败的尝试来满足这两个要求:

  1. 没有关于CorrectObject多余财产
  2. 可分配给HasX而不指定HasX作为对象的类型
type AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };

function objectLiteralAssignment() {
  const o: CorrectObject = {
    x: 1,
    y: "y",
    // z: "z" // z is correctly prevented to be defined for o by Excess Properties rules
  };

  const oAsHasX: HasX = o; // error: Types of property 'x' are incompatible.
}

function objectMultipleAssignment() {
  const o = {
    x: 1,
    y: "y",
    z: "z",
  };
  const o2 = o as CorrectObject; // succeeds, but undesirable property z is allowed

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

function genericExtends() {
  const checkType = <T>() => <U extends T>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    z: "z", // undesirable property z is allowed
  }); // o is inferred to be { x: number; y: string; z: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

这里HasX是一个大大简化的类型(实际类型将 o 映射到模式类型),它定义在与常量本身不同的层中,所以我不能创建o的类型成为( CorrectObject & HasX )。

使用精确类型,解决方案是:

function exactTypes() {
  const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    // z: "z", // undesirable property z is *not* allowed
  }); // o is inferred to be { x: number; y: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

@andy-ms

如果 T 是一个精确类型,那么大概 T & U 从不(或 T === U)。 对?

我认为T & U应该是never仅当U可证明与T不兼容时,例如,如果TExact<{x: number | string}>U{[field: string]: number} ,那么T & U应该是Exact<{x: number}>

看到第一个回复:

或者 U 是 T 的非精确子集

我会说,如果 U 可分配给 T,则T & U === T 。 但是如果TU是不同的确切类型,那么T & U === never

在您的示例中,为什么需要有一个什么都不做的checkType函数? 为什么不只有const o: Exact<CorrectObject> = { ... }

因为它丢失了 x 肯定存在(在 CorrectObject 中可选)和为数字(在 CorrectObject 中为数字 | 字符串)的信息。 或者我可能误解了 Exact 的意思,我认为它只会防止无关的属性,而不是递归地意味着所有类型必须完全相同。

支持精确类型和反对当前 EPC 的另一个考虑因素是重构 - 如果提取变量重构可用,除非提取的变量引入类型注释,否则会失去 EPC,这可能会变得非常冗长。

澄清为什么我支持精确类型 - 它不是用于可区分的联合,而是用于拼写错误和错误的无关属性,以防无法与对象文字同时指定类型 cotraint。

@andy-ms

我会说,如果 U 可以赋值给 T,那么 T & U === T。但是如果 T 和 U 是不同的确切类型,那么 T & U === 永远不会。

&类型运算符是交运算符,其结果是双方的公共子集,也不一定相等。 我能想到的最简单的例子:

type T = Exact<{ x?: any, y: any }>;
type U = { x: any, y? any };

这里T & U应该是Exact<{ x: any, y: any }> ,它的一个子集两者TU ,但既不T的一个子集U (缺少 x)和UT (缺少 y)的子集。

这应该独立于TUT & U是否是精确类型。

@magnushiie你有一个很好的观点——精确类型可以限制具有更大宽度的类型的可赋值性,但仍然允许具有更大深度的类型的可赋值性。 所以你可以将Exact<{ x: number | string }>Exact<{ x: string | boolean }>相交得到Exact<{ x: string }> 。 一个问题是,如果x不是只读的,那么这实际上不是类型安全的——我们可能希望针对确切类型修复该错误,因为它们意味着选择更严格的行为。

精确类型也可以用于索引签名的类型参数关系问题。

interface T {
    [index: string]: string;
}

interface S {
    a: string;
    b: string;
}

interface P extends S {
    c: number;
}

declare function f(t: T);
declare function f2(): P;
const s: S = f2();

f(s); // Error because an interface can have more fields that is not conforming to an index signature
f({ a: '', b: '' }); // No error because literals is exact by default

这是检查确切类型的一种hacky方法:

// type we'll be asserting as exact:
interface TextOptions {
  alignment: string;
  color?: string;
  padding?: number;
}

// when used as a return type:
function getDefaultOptions(): ExactReturn<typeof returnValue, TextOptions> {
  const returnValue = { colour: 'blue', alignment: 'right', padding: 1 };
  //             ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
  return returnValue
}

// when used as a type:
function example(a: TextOptions) {}
const someInput = {padding: 2, colour: '', alignment: 'right'}
example(someInput as Exact<typeof someInput, TextOptions>)
  //          ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.

不幸的是,目前无法将 Exact 断言作为类型参数,因此必须在调用时进行(即您需要记住它)。

以下是使其工作所需的辅助工具(感谢@tycho01提供的一些工具):

type Exact<A, B extends Difference<A, B>> = AssertPassThrough<Difference<A, B>, A, B>
type ExactReturn<A, B extends Difference<A, B>> = B & Exact<A, B>

type AssertPassThrough<Actual, Passthrough, Expected extends Actual> = Passthrough;
type Difference<A, Without> = {
  [P in DiffUnion<keyof A, keyof Without>]: A[P];
}
type DiffUnion<T extends string, U extends string> =
  ({[P in T]: P } &
  { [P in U]: never } &
  { [k: string]: never })[T];

参见:游乐场

好一个! @gcanti ( typelevel-ts ) 和@pelotom ( type-zoo ) 也可能感兴趣。 :)

对于任何感兴趣的人,我找到了一种对函数参数强制执行精确类型的简单方法。 至少适用于 TS 2.7。

function myFn<T extends {[K in keyof U]: any}, U extends DesiredType>(arg: T & U): void;

编辑:我想这个工作你必须直接在参数中指定一个对象文字; 如果您在上面声明一个单独的 const 并将其传入,这将不起作用。 :/ 但一种解决方法是在调用站点使用对象传播,即myFn({...arg})

编辑:对不起,我没有读到你只提到了 TS 2.7。 我会在那里测试它!

@vaskevich我似乎无法让它工作,即它没有将colour为多余的属性

当条件类型登陆 (#21316) 时,您可以执行以下操作以要求精确类型作为函数参数,即使对于“非新鲜”对象文字:

type Exactify<T, X extends T> = T & {
    [K in keyof X]: K extends keyof T ? X[K] : never
}

type Foo = {a?: string, b: number}

declare function requireExact<X extends Exactify<Foo, X>>(x: X): void;

const exact = {b: 1}; 
requireExact(exact); // okay

const inexact = {a: "hey", b: 3, c: 123}; 
requireExact(inexact);  // error
// Types of property 'c' are incompatible.
// Type 'number' is not assignable to type 'never'.

当然,如果您扩大类型,它将不起作用,但我认为您对此无能为力:

const inexact = {a: "hey", b: 3, c: 123} as Foo;
requireExact(inexact);  // okay

想法?

看起来函数参数正在取得进展。 有没有人找到一种方法来强制执行函数返回值的确切类型?

@jezzgoodwin不是真的。 参见 #241,这是函数返回没有正确检查额外属性的根本原因

再一个用例。 由于以下未报告为错误的情况,我几乎遇到了错误:

interface A {
    field: string;
}

interface B {
    field2: string;
    field3?: string;
}

type AorB = A | B;

const fixture: AorB[] = [
    {
        field: 'sfasdf',
        field3: 'asd' // ok?!
    },
];

游乐场

对此的明显解决方案可能是:

type AorB = Exact<A> | Exact<B>;

我在 #16679 中看到了一个解决方法,但在我的情况下,类型是AorBorC (可能会增长)并且每个对象都有多个属性,所以我很难手动计算fieldX?:never属性集对于每种类型。

@michalstocki不是#20863 吗? 您希望对工会的额外财产检查更加严格。

无论如何,在没有确切类型和对联合的严格过量属性检查的情况下,您可以通过使用条件类型以编程方式而不是手动执行这些fieldX?:never属性:

type AllKeys<U> = U extends any ? keyof U : never
type ExclusifyUnion<U> = [U] extends [infer V] ?
 V extends any ? 
 (V & {[P in Exclude<AllKeys<U>, keyof V>]?: never}) 
 : never : never

然后将您的工会定义为

type AorB = ExclusifyUnion<A | B>;

扩展到

type AorB = (A & {
    field2?: undefined;
    field3?: undefined;
}) | (B & {
    field?: undefined;
})

自动地。 它也适用于任何AorBorC

@jcalz高级类型 ExclusifyUnion 不是很安全:

const { ...fields } = o as AorB;

fields.field3.toUpperCase(); // it shouldn't be passed

fields的字段都是非可选的。

我认为这与 Exact 类型没有太大关系,而是与扩展然后解构联合类型的 object 时发生的情况有关。 任何联合最终都会被扁平化为一个类似交集的类型,因为它将一个对象分解为单独的属性,然后重新加入它们; 每个联合的组成部分之间的任何关联或约束都将丢失。 不知道如何避免它......如果它是一个错误,它可能是一个单独的问题。

显然,如果您在解构之前进行类型保护,事情会表现得更好:

declare function isA(x: any): x is A;
declare function isB(x: any): x is B;

declare const o: AorB;
if (isA(o)) {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
} else {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
  if (fields.field3) {
    fields.field3.toUpperCase(); // okay
  }
}

并不是说这“解决”了您看到的问题,而是我希望有人在受约束的工会中采取行动。

我可能会迟到,但这里是您至少可以确保您的类型完全匹配的方法:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};



declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType);  // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType);  // <-- problem, as expected

希望我能做到这一点:

type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;

此问题中描述的功能是否有助于处理空/索引接口与类对象类型(如函数或类)匹配的情况?

interface MyType
{
    [propName: string]: any;
}

function test(value: MyType) {}

test({});           // OK
test(1);            // Fails, OK!
test('');           // Fails, OK!
test(() => {});     // Does not fail, not OK!
test(console.log);  // Does not fail, not OK!
test(console);      // Does not fail, not OK!

接口MyType只定义了一个索引签名,作为test函数唯一参数的类型。 传递给函数类型的参数:

  • 对象文字{} ,通过。 预期行为。
  • 数字常量1未通过。 预期行为(“1”类型的 _Argument 不能分配给“MyType”类型的参数。_)
  • 字符串文字''未通过。 预期行为(_`'""' 类型的参数不可分配给'MyType' 类型的参数。_)
  • 箭头函数声明() => {} :通过。 不是预期的行为。 可能通过,因为函数是对象?
  • 类方法console.log通过。 不是预期的行为。 类似于箭头函数。
  • console通过。 不是预期的行为。 可能是因为类是对象?

关键是只允许与接口MyType完全匹配的变量已经是该类型(而不是隐式转换为它)。 TypeScript 似乎根据签名做了很多隐式转换,所以这可能是不支持的。

如果这是题外话,请见谅。 到目前为止,这个问题与我上面解释的问题最接近。

@Janne252这个提议可以间接帮助你。 假设您尝试了明显的Exact<{[key: string]: any}> ,这就是它起作用的原因:

  • 对象文字按预期传递,因为它们已经通过{[key: string]: any}传递。
  • 数字常量按预期失败,因为文字不能分配给{[key: string]: any}
  • 字符串文字按预期失败,因为它们不能分配给{[key: string]: any}
  • 函数和类构造函数由于它们的call签名(它不是字符串属性)而失败。
  • console对象通过,因为它只是一个对象(不是一个类)。 JS 没有区分对象和键/值字典,除了添加的行多态类型之外,TS 在这里也没有什么不同。 此外,TS 不支持值依赖类型,并且typeof只是用于添加一些额外参数和/或类型别名的糖 - 它并不像看起来那么神奇。

@blakeembrey @michalstocki @aleksey-bykov
这是我做精确类型的方式:

type Exact<A extends object> = A & {__kind: keyof A};

type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;

const takeFoo = (foo: Foo): Foo => foo;

const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;

takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
//  Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
//    Types of property '__kind' are incompatible.
//      Type '"foo" | "goo"' is not assignable to type '"foo"'.
//        Type '"goo"' is not assignable to type '"foo"'."

const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;

takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
//  Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
//    Property 'goo' is missing in type 'Exact<{ foo: number; }>'.

它适用于函数参数、返回甚至赋值。
const foo: Foo = fooGoo; // 错误
没有运行时开销。 唯一的问题是,每当您创建新的精确对象时,您都必须针对其类型进行强制转换,但这实际上并不是什么大问题。

我相信原始示例具有正确的行为:我希望interface s 是打开的。 相比之下,我希望type被关闭(它们只是有时被关闭)。 这是编写MappedOmit类型时令人惊讶的行为示例:
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3

示例中的MappedOmit类型仅适用于受歧视的联合。 对于非歧视联合,当联合中类型的任何交集被传递时,Typescript 3.2 就会被传递。

上述使用as TypeXas any进行强制转换的变通方法具有隐藏构造错误的副作用!。 我们希望我们的类型检查器也能帮助我们发现构造中的错误! 此外,我们可以从定义良好的类型静态生成一些东西。 像上面这样的变通方法(或这里描述的名义类型变通方法:https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481)阻止这些生成器工作(尽管我们可以过滤_前导字段,这是一个痛苦的约定这是绝对可以避免的)。

@aleksey-bykov fyi 我认为你的实现已经完成了 99%,这对我有用:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

const value1 = {};
const value2 = {a:1};

// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;

// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;

// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;

// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;

哇,请把花留在这儿,礼物放在那个箱子里

可以在此处进行的一项小改进:
通过使用Exact的以下定义,可以有效地从A减去B A作为Anever类型的所有B的唯一键,您可以在无效属性上获得更细粒度的错误:

type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;

最后,我希望能够做到这一点,而不必添加第二个B模板参数的显式模板用法。 我能够通过包装一个方法来完成这项工作 - 不理想,因为它会影响运行时,但如果你真的需要它,它很有用:

function makeExactVerifyFn<T>() {
  return <C>(x: C & Exact<T, C>): C => x;
}

示例用法:

interface Task {
  title: string;
  due?: Date;
}

const isOnlyTask = makeExactVerifyFn<Task>();

const validTask_1 = isOnlyTask({
    title: 'Get milk',
    due: new Date()  
});

const validTask_2 = isOnlyTask({
    title: 'Get milk'
});

const invalidTask_1 = isOnlyTask({
    title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});

const invalidTask_2 = isOnlyTask({
    title: 'Get milk',
    procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});

@danielnmsft在您的示例中将B中的Exact<A, B>保留B可选似乎很奇怪,尤其是在正确验证需要它的情况下。 否则,它在我看来还不错。 不过,它看起来更好命名Equal

@drabinowitz您的类型Exact实际上并不代表此处提出的内容,可能应该重命名为AreExact 。 我的意思是,你不能用你的类型做到这一点:

function takesExactFoo<T extends Exact<Foo>>(foo: T) {}

但是,您的类型可以方便地实现确切的参数类型!

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

interface Foo {
    bar: any
}

function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
                    //  ^ or `T extends Foo` to type-check `foo` inside the function
}

let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}

takesExactFoo(foo) // ok
takesExactFoo(foo2) // error

UPD1 这不会像@danielnmsft解决方案那样创建 +1 运行时函数,当然更灵活。

UPD2 我刚刚意识到 Daniel 实际上制作了与@drabinowitz所做的基本相同的类型Exact ,但更紧凑,可能更好。 我也意识到我做了和丹尼尔一样的事情。 但如果有人觉得有用,我会留下我的评论。

AreSame / Exact似乎不适用于联合类型。
示例: Exact<'a' | 'b', 'a' | 'b'>结果为never
这显然可以通过定义type AreSame<A, B> = A|B extends A&B ? true : false;

@nerumo确实为您展示的同一类型的 reducer 函数找到了这个。

从你所拥有的几个额外的选项:

1 您可以使用typeof将返回类型设置为与输入类型相同。 如果它是一个非常复杂的类型,则更有用。 对我来说,当我看到这个时,更明显的意图是防止额外的属性。

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
   return {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   };
}

2 对于reducers,不是临时变量,在返回之前将其赋值给自身:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {
   return (state = {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   });
}

3 如果你真的想要一个临时变量,不要给它一个明确的类型,再次使用typeof state

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: typeof state = {
       ...state,
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

3b 如果您的减速器不包含...state您可以使用Partial<typeof state>作为类型:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: Partial<typeof state> = {
       name: 'Simon',
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

我确实觉得整个对话(我只是阅读了整个线程)错过了大多数人的问题的症结所在,那就是为了防止错误,我们想要的只是一个类型断言,以防止禁止“更广泛”的类型:

这是人们可能首先尝试的方法,它不允许使用“fullName”:

 return <State> {
       ...state,
       fullName: action.payload         // compiles ok :-(
   };

这是因为您告诉编译器<Dog> cat - 是的,我知道我在做什么,它是Dog ! 你不是在征求许可。

所以对我来说最有用的是更严格的<Dog> cat版本,它可以防止无关的属性:

 return <strict State> {
       ...state,
       fullName: action.payload     // compiles ok :-(
   };

整个Exact<T>类型的事情有很多影响后果(这是一个长线!)。 它让我想起了整个“检查异常”辩论,在那里它是你认为你想要的东西,但事实证明它有很多问题(比如五分钟后突然想要一个Unexact<T> )。

另一方面, <strict T>更像是一个障碍,以防止“不可能”的类型“通过”。 它本质上是一个通过类型传递的类型过滤器(正如上面对运行时函数所做的那样)。

然而,新手很容易假设它可以防止“坏数据”在不可能做到的情况下通过。

因此,如果我必须制定提案语法,它将是这样的:

/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };

回到OP:理论上[1]使用否定类型你可以写type Exact<T> = T & not Record<not keyof T, any> 。 然后Exact<{x: string}>将禁止任何键值不是x分配给它。 不确定这是否足以满足这里每个人的要求,但它似乎非常适合 OP。

[1] 我说理论上是因为这也是基于更好的索引签名

很想知道我是否有此处描述的问题。 我有这样的代码:

const Layers = {
  foo: 'foo'
  bar: 'bar'
  baz: 'baz'
}

type Groups = {
  [key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}

const groups = {} as Groups

然后它允许我设置未知属性,这是我不想要的:

groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(

设置anything仍然有效,键值类型为any 。 我希望这会是一个错误。

这是这个问题将要解决的吗?

原来,我应该做的

type Groups = {
  [key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}

注意typeof的附加用法。

Atom 插件atom-typescript努力避免失败,最终崩溃了。 当我添加typeof ,事情恢复正常,不再允许使用未知的道具,这正是我所期待的。

换句话说,当我不使用typeofatom-typescript试图在我使用Groups类型的对象的代码的其他地方计算类型,并且它允许我添加未知的道具并向我显示any的类型提示。

所以我不认为我有这个线程的问题。

另一个复杂问题可能是如何处理可选属性。

如果您的类型具有可选属性,那么这些属性的Exact<T>意味着:

export type PlaceOrderResponse = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharged?: number
};

Exact<T>意味着必须定义每个可选属性? 你会把它指定为什么? 不是 'undefined' 或 'null' 因为它具有运行时效果。

现在是否需要一种新方法来指定“必需的可选参数”?

例如,在以下代码示例中,我们必须为amountCharged分配什么才能使其满足类型的“精确性”? 如果我们不强制此属性至少以某种方式“确认”,我们就不是很“精确”。 是<never>吗? 它不能是undefinednull

const exactOrderResponse: Exact<PlaceOrderResponse> = 
{
   status: 'paymentFailed',
   orderNumber: '1001',
   amountCharged: ????      
};

所以你可能会想 - 它仍然是可选的,现在它是完全可选的,只是转换为optional 。 当然在运行时它不需要设置,但在我看来,我们只是通过粘贴问号来“破坏” Exact<T>

也许只有在两种类型之间赋值时才需要进行这种检查? (为了强制它们都包含amountCharged?: number

让我们在这里为对话框的输入数据引入一种新类型:

export type OrderDialogBoxData = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharge?: number      // note the typo here!
};

所以让我们试试这个:

// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse;    // SHOULD FAIL

我当然希望这会因为打字错误而失败 - 即使这个属性在两者中都是可选的。

然后我又回到了'为什么我们首先想要这个? .
我认为这将是出于这些原因(或视情况而定的一个子集):

  • 避免输入错误的属性名称
  • 如果我们向某个“组件”添加一个属性,我们希望确保使用它的所有内容也必须添加该属性
  • 如果我们从某个“组件”中删除一个属性,我们需要在任何地方删除它。
  • 确保我们不会提供不必要的额外属性(也许我们将其发送到 API 并且我们希望保持有效负载精简)

如果“精确的可选属性”处理不当,那么这些好处中的一些就会被破坏或非常混乱!

同样在上面的例子中,我们只是'shoehorned' Exact试图避免拼写错误,但只是成功地把它弄得一团糟! 现在它比以往任何时候都更加脆弱。

我认为我经常需要的实际上根本不是Exact<T>类型,它是以下两者之一:

NothingMoreThan<T>
NothingLessThan<T>

'必需的可选'现在是一回事。 第一个不允许分配的 RHS 定义任何额外内容,第二个确保在分配的 RHS 上指定所有内容(包括可选属性)。

NothingMoreThan对通过网络发送的有效载荷很有用,或者JSON.stringify()如果您因为 RHS 上的属性太多而出现错误,则必须编写运行时代码才能仅选择所需的属性。 这就是正确的解决方案 - 因为这就是 Javascript 的工作方式。

NothingLessThan是我们在打字稿中已经拥有的东西——对于所有正常的赋值——除了它需要考虑可选的(optional?: number)属性。

我不希望这些名字有任何吸引力,但我认为这个概念比Exact<T>更清晰、更精细......

然后,也许(如果我们真的需要它):

Exact<T> = NothingMoreThan<NothingLessThan<T>>;

或者它会是:

Exact<T> = NothingLessThan<NothingMoreThan<T>>;   // !!

这篇文章是我今天遇到的一个实际问题的结果,我有一个包含一些可选属性的“对话框数据类型”,我想确保来自服务器的内容可以分配给它。

最后一点: NothingLessThan / NothingMoreThan与上面的一些注释有类似的“感觉”,其中类型 A 从类型 B 扩展,或 B 从 A 扩展。那里的限制是它们不会解决可选属性(至少我认为今天不能)。

@simeyla您可以摆脱“无外乎”的变体。

  • “不低于”只是普通类型。 TS 隐式地执行此操作,并且每个类型都被视为等效于for all T extends X: T
  • "Nothing more than" 基本上是相反的:它是一个隐含的for all T super X: T

一种明确选择一个或两个的方法就足够了。 作为副作用,您可以将 Java 的T super C为您建议的T extends NothingMoreThan<C> 。 所以我非常确信这可能比标准的精确类型更好

我觉得这应该是语法。 也许这个?

  • extends T - 可分配给 T 的所有类型的并集,即相当于普通的T
  • super T - 所有类型 T 的联合都是可分配的。
  • extends super T , super extends T - 等价于 T 的所有类型的联合。这只是脱离了网格,因为只有类型既可以赋值又可以赋值给自身。
  • type Exact<T> = extends super T - 内置糖用于上述常见情况,以提高可读性。
  • 由于这只是切换可分配性,因此您仍然可以拥有诸如精确类型或超类型联合之类的东西。

这也使得只需让每个变体Exact<T>就可以在用户空间中实现 #14094 ,例如Exact<{a: number}> | Exact<{b: number}>


我想知道这是否也使用户空间中的否定类型成为可能。 我相信它确实如此,但我需要先做一些复杂的类型算术来确认这一点,而且要证明这一点并不是一件显而易见的事情。

我想知道这是否也使用户空间中的否定类型成为可能,因为 (super T) | (extends T) 等价于未知。 我相信是这样,但我需要先做一些复杂的类型算术来确认这一点,而且这并不是一个显而易见的证明。

对于(super T) | (extends T) === unknown持有可分配性将需要是一个总订单。

@jack-williams 很好地捕获并修复了(通过删除声明)。 我想知道为什么一开始我玩的时候事情不顺利。

@杰克威廉姆斯

“不低于”只是普通类型。 TS 隐式执行此操作,并且每个类型都被视为等效

是和否。 但大多数情况下是的... ...但前提是您处于strict模式!

所以我在很多情况下需要一个属性在逻辑上是“可选的”,但我希望编译器告诉我我是否“忘记了它”或拼错了它。

嗯,这正是你用lastName: string | undefined得到的,而我大部分得到的是lastName?: string ,当然如果没有strict模式,你不会被警告所有的差异。

我一直都知道严格模式,而且我一生都找不到一个很好的理由为什么我直到昨天才打开它 - 但现在我知道了(而且我仍在经历数百个修复) 获得我想要的“开箱即用”的行为要容易得多。

我一直在尝试各种方法来获得我想要的东西 - 包括玩Required<A> extends Required<B> ,并尝试删除可选的?属性标志。 这让我陷入了一个完全不同的兔子洞 - (这是在我打开strict模式之前的全部内容)。

关键是,如果您今天试图获得接近“精确”类型的东西,那么您需要从启用strict模式开始(或任何标志组合提供正确的检查)。 如果我需要稍后添加middleName: string | undefined然后繁荣 - 我会突然找到我需要“考虑”的地方:-)

附注。 感谢您的评论 - 非常有帮助。 我意识到我已经看到很多代码显然没有使用strict模式 - 然后人们像我一样撞到墙。 我想知道可以做些什么来鼓励更多地使用它?

@simeyla我认为您的反馈和感谢应该针对@isiahmeadows!

我想我会在实现一个基本原型后写下我对 Exact 类型的体验。 我的总体想法是,该团队的评估是正确的:

我们希望的诊断是,除了相对较少的真正封闭的 API 之外,这是一个 XY 问题解决方案。

我不认为通过捕获更多错误或启用新类型关系来偿还引入另一种对象类型的成本。 最终,确切的类型让我_说_更多,但他们并没有让我_做_更多。

检查精确类型的一些潜在用例:

keysfor ... in强类型。

在枚举键时使用更精确的类型似乎很有吸引力,但在实践中,我从未发现自己为概念上精确的事物枚举键。 如果您确切地知道密钥,为什么不直接解决它们呢?

硬化可选属性加宽。

可分配性规则{ ... } <: { ...; x?: T }是不合理的,因为左类型可能包含一个不兼容的x属性,该属性被别名掉了。 当从一个确切的类型分配时,这个规则变得合理。 在实践中我从不使用这个规则; 它似乎更适合没有确切类型的遗留系统。

反应和 HOC

我把最后的希望寄托在精确类型改进道具传递和简化传播类型上。 现实情况是,精确类型是有界多态性的对立面,并且基本上是非组合的。

有界泛型允许您指定您关心的道具,并通过其余的道具。 一旦边界变得精确,您将完全失去宽度子类型,并且泛型变得不那么有用了。 另一个问题是 TypeScript 中的主要组合工具之一是交集,但交集类型与精确类型不兼容。 任何具有精确组件的非平凡交叉类型都将是空的:_精确类型不会组合_。 对于 react 和 props,您可能需要行类型和行多态性,但那是另一天。

几乎所有可以通过精确类型解决的有趣错误都通过额外的属性检查来解决; 最大的问题是,多余的财产检查对没有判别财产的工会不起作用; 解决这个问题,几乎所有与精确类型相关的有趣问题都会消失,IMO。

@jack-williams 我同意拥有确切类型通常不是很有用。 多余的属性检查概念实际上包含在我的super T运算符提案中,只是间接地因为所有类型 T 的联合可分配给明显包括 T 的正确子类型。

除了可能T super U * 之外,我个人并没有大力支持这一点,因为我遇到的唯一

* 使用我的建议,这基本上是T extends super U - 下限有时对于约束逆变泛型类型很有用,根据我的经验,变通方法通常最终会引入很多额外的类型样板。

@isiahmeadows我当然同意下限类型很有用,如果你能从中得到确切的类型,那么对于那些想要使用它们的人来说,这是一个胜利。 我想我应该在我的帖子中添加一个警告:我主要解决添加一个专门用于精确对象类型的新运算符的概念。

@jack-williams 我想你错过了我的细微差别,我主要是指过度属性检查的确切类型和相关部分。 关于下界类型的一点是出于某种原因的脚注 - 这是一个仅与切线相关的题外话。

我设法为此编写了一个实现,它适用于需要不同程度精确性的函数参数:

// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
   [P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}

// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }

// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 })        //Fine
noMore({ a: 5, b: 4 })           //Error 
noMore({ a: "o", b: "hello" })   //Error
noMore({ a: "o" })               //Fine
noMore({ b: 4 })                 //Fine
noMore({ a: "o", b: 4, c: 5 })   //Error

// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 })        //Fine
noLess({ a: 5, b: 4 })           //Error
noLess({ a: "o", b: "hello" })   //Error
noLess({ a: "o" })               //Error  |b?: Fine
noLess({ b: 4 })                 //Error
noLess({ a: "o", b: 4, c: 5 })   //Fine

// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 })        //Fine
strict({ a: 5, b: 4 })           //Error
strict({ a: "o", b: "hello" })   //Error
strict({ a: "o" })               //Error  |b?: Fine
strict({ b: 4 })                 //Error
strict({ a: "o", b: 4, c: 5 })   //Error

// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 })        //Fine
permissive({ a: 5, b: 4 })           //Error
permissive({ a: "o", b: "hello" })   //Error
permissive({ a: "o" })               //Fine
permissive({ b: 4 })                 //Fine
permissive({ a: "o", b: 4, c: 5 })   //Fine


我意识到变量赋值的确切类型实际上并没有做任何事情......

// This is a little unweildy, there's also a shortform that works in many cases:
type Exact<A extends {}> = Subset<A, A>
// The simpler Exact type works for variable typing
const options0: Exact<BaseOptions> = { a: "hi", b: 4 }        //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 }           //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" }   //Error
const options3: Exact<BaseOptions> = { a: "o" }               //Error |b?: Fine
const options4: Exact<BaseOptions> = { b: 4 }                 //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 }   //Error

// It also works for function typing when using an inline value
const exact = (options: Exact<BaseOptions>) => { }
exact({ a: "hi", b: 4 })        //Fine
exact({ a: 5, b: 4 })           //Error
exact({ a: "o", b: "hello" })   //Error
exact({ a: "o" })               //Error  |b?: Fine
exact({ b: 4 })                 //Error
exact({ a: "o", b: 4, c: 5 })   //Error

// But not when using a variable as an argument even of the same type
const options6 = { a: "hi", b: 4 }
const options7 = { a: 5, b: 4 }
const options8 = { a: "o", b: "hello" }
const options9 = { a: "o" }
const options10 = { b: 4 }
const options11 = { a: "o", b: 4, c: 5 }
exact(options6)                 //Fine
exact(options7)                 //Error
exact(options8)                 //Error
exact(options9)                 //Error |b?: Fine
exact(options10)                //Error
exact(options11)                //Fine  -- Should not be Fine

// However using strict does work for that
// const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict(options6)                //Fine
strict(options7)                //Error
strict(options8)                //Error
strict(options9)                //Error |b?: Fine
strict(options10)               //Error
strict(options11)               //Error -- Is correctly Error

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

我觉得在包装 React 组件时我有一个用例,我需要“通过”道具: https :

@OliverJAsh看起来很相关,但我必须承认我不像大多数人那样了解 React。 我想了解确切类型如何在这里准确提供帮助会很有帮助。

type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;

type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
    <MyComponent
        // We're passing too many props here, but no error!
        {...props}
    />
);

请在任何时候纠正我我说错了什么。

我猜一开始是指定MyComponent来接受一个确切的类型?

declare const MyComponent: ComponentType<Exact<MyComponentProps>>;

在这种情况下,我们会得到一个错误,但是你如何修复这个错误呢? 我在这里假设包装器组件不仅具有相同的 prop 类型,而且在某些时候您确实需要动态提取一个 prop 子集。 这是一个合理的假设吗?

如果MyWrapperComponent props 也是精确的,那么我认为进行解构绑定就足够了。 在一般情况下,这需要Omit类型而不是精确类型,我真的不知道那里的语义。 我猜它可以像同态映射类型一样工作并保持精确性,但我认为这需要更多的思考。

如果MyWrapperComponent不精确,那么它将需要一些运行时检查来证明新类型的正确性,这只能通过明确选择所需的属性来完成(这不会像你说的那样缩放)你的操作)。 我不确定你在这种情况下获得了多少。

我没有涉及的事情,因为我不知道它们是泛型情况的可能性有多大,其中props是某种泛型类型,并且您需要组合像{ ...props1, ...props2 }这样的道具。 这是常见的吗?

@Kotarski您是否有机会在 NPM 注册表中发布它?

@gitowiec

@Kotarski您是否有机会在 NPM 注册表中发布它?

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

我有这个用例:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };

// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };

@ryami333不需要确切的类型; 只需要修复多余的属性检查:#13813。

@ryami333如果你愿意使用额外的类型,我有一个类型可以做你想做的事情,即强制更严格的联合版本:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD


type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };

@dragomirtitian迷人。 我很好奇为什么

type KeyofV1<T extends object> = keyof T

产生不同的结果

type KeyofV2<T> = T extends object ? keyof T : never

有人可以向我解释一下吗?

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'

V1获取联合的公共键, V2获取每个联合成员的键并联合结果。

@weswigham他们是否有理由返回不同的结果?

是的? 正如我所说 - V1为每个工会成员获取 _common keys_,因为keyof最终是keyof (AB | ABCD) ,这只是"A" | "B" ,而条件中的版本一次只接收一个联合成员,这要归功于其输入的条件分布,所以它本质上是keyof AB | keyof ABCD

@weswigham所以条件更像这样评估它,比如通过一些隐式循环?

type Union =
    (AB extends object ? keyof AB : never) |
    (ABCD extends object ? keyof ABCD : never)

当我阅读该代码时,我通常希望(AB | ABCD) extends object检查作为单个单元运行,检查(AB | ABCD)是否可分配给object ,然后返回keyof (AB | ABCD)作为一个单位, 'a' | 'b' 。 隐式映射对我来说似乎很奇怪。

@isiahmeadows您可以将分配条件类型视为联合的 foreach。 他们依次将条件类型应用于联合的每个成员,结果是每个部分结果的联合。

所以UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)

但仅当条件类型分布时,并且仅当测试类型是裸类型参数时才分布。 所以:

type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type

谢谢大家,我想我明白了。 为了我的理解,我重新安排了它; 我相信NegativeUncommonKeys本身也很有用。 以防万一它对其他人也有用。

type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
    Partial<
        Record<
            Exclude<
                UnionKeys<TAll>,
                keyof T
            >,
            never
        >
    >
) 

type StrictUnion<T, TAll = T> = T extends any 
  ? T & NegateUncommonKeys<T, TAll>
  : never;

我也明白为什么TTAll都在那里。 “循环效应”,其中 T 被测试和裸露,意味着应用T联合中的每个项目,而未测试的TAll包含所有项目的原始和完整联合。

@weswigham是的 .. 除了我觉得这部分读起来像是由一位编译器工程师为另一位编译器工程师编写的。

被检查类型是裸类型参数的条件类型称为分布条件类型。

什么是裸类型参数? (还有他们为什么不穿点衣服😄)

即 T 指的是条件类型分布在联合类型上的单个成分)

就在昨天,我讨论了这个特定句子的含义以及为什么要强调“之后”这个词。

我认为文档是在假设用户可能并不总是拥有的先验知识和术语的情况下编写的。

手册部分对我来说确实有意义,它解释得更好,但我仍然对那里的设计选择持怀疑态度。 对我来说,从集合理论和类型理论的角度来看,这种行为如何自然地遵循在逻辑上是没有意义的。 它只是让人觉得有点太hackish了。

从集合论和类型论的角度自然地遵循

取集合中的每一项并根据谓词对其进行分区。

那是分布式操作!

取集合中的每一项并根据谓词对其进行分区。

尽管这仅在您谈论开始听起来更像范畴论的集合(即联合类型)时才有意义。

@RyanCavanaugh好的,让我澄清一下:我直观地将T extends U ? F<T> : G<T>读作T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T) ,比较不是分段进行的,而是作为一个完整的步骤。 这与“所有{if t ∈ U then F({t}) else G({t}) | t ∈ T}的并集”明显不同,后者是当前的语义。

(请原谅我的语法有点不对劲——我的类型论知识完全是自学的,所以我知道我不知道所有的句法形式。)

哪种操作更直观有待无限讨论,但根据当前规则,使用[T] extends [C]很容易使分配类型成为非分配类型。 如果默认设置是非分配性的,则您需要在不同级别上使用一些新咒语来实现分配性。 这也是一个单独的问题,哪些行为更常被首选; IME 我几乎从不想要非分布式类型。

是的,分布没有强大的理论基础,因为它是一种句法操作。

现实情况是它非常有用,尝试以其他方式对其进行编码会很痛苦。

就目前而言,在我将谈话推离主题之前,我会继续前进并逐渐结束。

已经有很多关于分布式的问题,为什么我们不面对需要新语法的问题?

30572

这是一个示例问题:

我想指定我的用户 API 端点/服务不能返回除服务接口中指定的属性之外的任何额外属性(例如密码)。 如果我不小心返回了一个具有额外属性的对象,我想要一个编译时错误,无论结果对象是由对象文字还是其他方式生成的。

对每个返回对象的运行时检查可能代价高昂,尤其是对于数组。

在这种情况下,过多的属性检查无济于事。 老实说,我认为这是一个古怪的一招式解决方案。 从理论上讲,它应该提供一种“它只是有效”的体验 - 在实践中,它也是混淆的根源应该实现确切的对象类型,它们会很好地涵盖这两个用例。

@babakness您的类型NoExcessiveProps是空操作。 我认为他们的意思是这样的:

interface API {
    username: () => { username: string }
}

const api: API = {
    username: (): { username: string } => {
        return { username: 'foobar', password: 'secret'} // error, ok
    }
}

const api2: API = {
    username: (): { username: string } => {
        const id: <X>(x: X) => X = x => x;
        const value = id({ username: 'foobar', password: 'secret' });
        return value  // no error, bad?
    }
}

作为 API 类型的作者,您希望强制username只返回用户名,但任何实现者都可以解决这个问题,因为对象类型没有宽度限制。 这只能应用于文字的初始化,实现者可能会也可能不会这样做。 尽管如此,我强烈建议任何人不要尝试使用精确类型作为基于语言的安全性。

@spion

在这种情况下,过多的属性检查无济于事。 老实说,我认为这是一个古怪的一招式解决方案。 从理论上讲,他们应该提供“它只是有效”的体验

EPC 是一个合理合理的轻量级设计选择,涵盖了大量问题。 现实情况是 Exact 类型并不“只是工作”。 要以支持可扩展性的健全方式实现,需要完全不同的类型系统。

@jack-williams 当然,还有其他方法来验证存在(运行时检查性能不是问题,测试等),但额外的编译时方法对于快速反馈是非常宝贵的。

另外,我并不是说确切的类型“可以正常工作”。 我的意思是 EPC 旨在“正常工作”,但实际上它只是有限的、混乱的和不安全的。 主要是因为如果尝试“故意”使用它,您通常最终会用脚射击自己。

编辑:是的,当我意识到它令人困惑时,我进行了编辑以将“他们”替换为“它”。

@spion

另外,我并不是说确切的类型“可以正常工作”。 我的意思是 EPC 旨在“正常工作”,但实际上它只是有限的、混乱的和不安全的。 主要是因为如果尝试“故意”使用它,您通常最终会用脚射击自己。

我的错。 阅读原始评论为

从理论上讲,他们应该提供一种“它只是有效”的体验[这将是精确类型而不是 EPC]

[]中的评论是我的阅读。

修改后的声明:

从理论上讲,应该提供“它只是有效”的体验

清楚多了。 对不起,我的误解!

type NoExcessiveProps<O> = {
  [K in keyof O]: K extends keyof O ? O[K] : never 
}

// no error
const getUser1 = (): {username: string} => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
} 

// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}>  = {username: 'a', password: 'b' }

// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
}


getUser2的结果令人惊讶,感觉不一致,并且应该产生编译时错误。 关于为什么不这样做的见解是什么?

@babakness您的NoExcessiveProps只是计算回T (以及与T具有相同键的类型)。 在[K in keyof O]: K extends keyof O ? O[K] : neverK将始终是O的键,因为您正在映射keyof O 。 您的const示例错误,因为它会触发 EPC,就像您将其输入为{username: string}

如果您不介意调用额外的函数,我们可以捕获传入的对象的实际类型,并执行自定义形式的多余属性检查。 (我确实意识到重点是自动捕获此类错误,因此这可能价值有限):

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked(foo) //ok
}

@dragomirtitian啊......对......好点! 所以我试图理解你的checked函数。 我特别疑惑

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    const bar = checked(foo) // error
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    const bar = checked(foo) // error!?
    return checked(foo) //ok
}

bar赋值getUser3失败。 错误似乎在foo
image

错误详情

image

这里bar的类型是{} ,这似乎是因为checked

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

E未在任何地方分配。 然而,如果我们用typeof E替换typeof {} ,它就不起作用。

E 的类型是什么? 是否发生了某种上下文感知的事情?

@babakness如果没有其他地方可以推断类型参数,打字稿将从返回类型推断它。 因此,当我们将checked的结果分配给getUser*的返回值时, E将是函数的返回类型,而T将是要返回的值的实际类型。 如果没有地方可以从中推断出E ,它将默认为{} ,因此您将始终收到错误消息。

我这样做的原因是为了避免任何显式类型参数,您可以创建一个更显式的版本:

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked<{ username: string }>()(foo) //ok
}

注意:柯里化函数方法是必要的,因为我们还没有部分参数推断(https://github.com/Microsoft/TypeScript/pull/26349),所以我们不能指定某些类型参数并在同样的电话。 为了解决这个问题,我们在第一次调用中指定了E ,并在第二次调用中推断出T 。 您还可以缓存特定类型的cache函数并使用缓存版本

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}
const checkUser = checked<{ username: string }>()

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checkUser(foo) //ok
}

FWIW 这是一个 WIP/sketch tslint 规则,它解决了不会意外地从“公开”方法中返回额外属性的特定问题。

https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32

对于传播用例 - 请参阅https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -300382189 - linter 能否检测到这样的模式并警告它不是类型安全的?

从上述评论中复制代码示例:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

抄送@JamesHenry / @armano2

非常希望看到这种情况发生。 我们为 GraphQL 端点使用生成的 TypeScript 定义,这是一个问题,当我向查询传递包含更多字段的对象时,TypeScript 不会引发错误,因为 GraphQL 将无法在运行时执行此类查询。

现在通过 3.5.1 更新解决了多少问题,并在分配期间更好地检查额外属性? 在升级到 3.5.1 后,我们按照我们希望的方式将一堆已知问题区域标记为错误

如果您遇到问题并且您认为精确类型是正确的解决方案,请在此处描述原始问题

https://github.com/microsoft/TypeScript/issues/12936#issuecomment -284590083

这是一个涉及 React refs 的: https :

/cc @瑞安卡瓦诺

我的一个用例是

export const mapValues =
  <T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
    type TResult = Exact<{ [K in keyof T]: V }>;
    const result: Partial<TResult> = { };
    for (const [key, value] of Object.entries(object)) {
      result[key] = mapper(value, key);
    }
    return result as TResult;
  };

如果我们不使用精确类型,这是不合理的,因为如果object有额外的属性,那么在这些额外的键和值上调用mapper是不安全的。

这里的真正动机是我想在某个地方拥有可以在代码中重用的枚举值:

const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));

其中this.props.callback类型(keyof typeof choices) => void

所以实际上是关于类型系统能够表示这样一个事实,即我在代码域中有一个键列表,它与类型域中的键集(例如联合)完全匹配,以便我们可以编写对此进行操作的函数键列表并对结果进行有效的类型断言。 我们不能使用对象(在我前面的例子中为choices ),因为就类型系统而言,代码域对象可能具有超出使用的任何对象类型的额外属性。 我们不能使用数组( ['choice0', 'choice1', 'choice2'] as const ,因为据类型系统所知,数组可能不包含数组类型允许的所有键。

也许exact不应该是类型,而应该是函数输入和/或输出的修饰符? 类似于流的方差修饰符( + / -

我想补充@phaux刚才所说的话。 我对Exact的真正用途是让编译器保证函数的形状。 当我有一个框架时,我可能需要以下任何一个: (T, S): AtMost<T>(T, S): AtLeast<T>(T, S): Exact<T>编译器可以验证用户定义的函数是否完全适合。

一些有用的例子:
AtMost对配置很有用(所以我们不会忽略额外的参数/错别字并提前失败)。
AtLeast非常适合诸如反应组件和中间件之类的东西,用户可以将他们想要的任何额外内容推送到对象上。
Exact对序列化/反序列化很有用(我们可以保证我们不会丢弃数据并且这些是同构的)。

这是否有助于防止这种情况发生?

interface IDate {
  year: number;
  month: number;
  day: number;
}

type TBasicField = string | number | boolean | IDate;

 // how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
  // ... do things with basic field of only the exactly correct structures
}

const notADate = {
  year: 2019,
  month: 8,
  day: 30,
  name: "James",
};

doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking

我们真的需要一种在 TS 中说T extends exactly { something: boolean; } ? xxx : yyy

否则,例如:

const notExact = {
  something: true,
  name: "fred",
};

仍然会在那里返回xxx

也许可以使用const关键字? 例如T extends const { something: boolean }

@pleerock它可能有点含糊不清,因为在 JavaScript / TypeScript 中,我们可以将变量定义为const但仍然添加/删除对象属性。 我认为关键字exact很中肯。

我不确定它是否完全相关,但我预计在这种情况下至少会出现两个错误:
操场
Screen Shot 2019-08-08 at 10 15 34

@mityok我认为这是相关的。 我猜你想做一些类似的事情:

class Animal {
  makeSound(): exact Foo {
     return { a: 5 };
  }
}

如果exact使类型更严格 - 那么它不应该使用额外的属性进行扩展,就像你在Dog所做的那样。

利用const ( as const ) 并在接口和类型之前使用,例如

const type WillAcceptThisOnly = number

function f(accept: WillAcceptThisOnly) {
}

f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast

const n: WillAcceptThisOnly = 1
f(n) // ok

必须分配给 const 变量会非常冗长,但是当您传递不完全符合您期望的类型别名时,会避免很多边缘情况

我为Exact<T>问题提出了纯 TypeScript 解决方案,我相信,它的行为与主要帖子中的要求完全相同:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

ExactInner不得包含在Exact中的原因是 #32824 修复尚未发布(但已合并到!32924 中)。

只能为Exact<T>类型的变量或函数参数赋值,如果右手表达式也是Exact<T> ,其中T的两个部分的类型完全相同的任务。

我还没有实现将值自动提升为 Exact 类型,所以这就是exact()辅助函数的用途。 任何值都可以提升为精确类型,但只有当 TypeScript 能够证明表达式的两个部分的基础类型不仅可扩展,而且完全相同时,赋值才会成功。

它的工作原理是利用 TypeScript 使用extend关系检查来确定是否可以将右手类型分配给左手类型——只有当右手类型(源)_扩展_左手类型(目标)时才可以.

引用checker.ts

// 两个条件类型 'T1 extends U1 ? X1 : Y1' 和 'T2 延伸 U2 ? X2 : Y2' 相关,如果
// T1和T2之一与另一个相关,U1和U2是相同类型,X1与X2相关,
// 并且 Y1 与 Y2 相关。

ExactInner<T>泛型使用所描述的方法,将U1U2替换为需要正确性检查的基础类型。 Exact<T>添加了一个与普通底层类型的交集,这允许 TypeScript 在其目标变量或函数参数不是精确类型时放宽精确类型。

从程序员的角度来看, Exact<T>行为就好像它在T上设置了exact标志,而不检查T或更改它,并且不创建独立类型。

这里是操场链接要点链接

未来可能的改进是允许将非精确类型自动提升为精确类型,完全消除exact()函数的需要。

惊人的工作@toriningen!

如果有人能够找到一种方法来完成这项工作,而不必将您的价值包装在对exact的调用中,那将是完美的。

不确定这是否是正确的问题,但这里有一个我想要工作的例子。

https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAu-DNlzAGEBCATwAU-DN5SHAQUlqa0FmAhyCauvCavlqAuFuFmAhBhyCavlqAuFuFmAhqaqauFuFmAhqyygaqabofmaqaubwcgoobbazyygf4obyaq2oybwcgoobbazyygf4obyaq2o

enum SortDirection {
  Asc = 'asc',
  Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());

@lookfirst那是不同的。 这要求为不接受额外属性的类型提供功能,例如某些类型exact {foo: number} ,其中{foo: 1, bar: 2}不可分配给它。 这只是要求将文本转换应用于枚举值,而枚举值可能不存在。

不确定这是否是正确的问题,但是 [...]

根据我作为其他地方的维护人员的经验,如果您有疑问并且找不到任何明确的现有问题,请提交新的错误和最坏的情况,它会作为您没有找到的骗局而关闭。 大多数主要的开源 JS 项目几乎都是这种情况。 (我们 JS 社区中的大多数大型维护者实际上都是体面的人,只是那些可以真正陷入错误报告之类的人,因此有时很难不真正简洁。)

@isiahmeadows感谢您的回复。 我没有提交新问题,因为我首先要搜索重复的问题,这是正确的做法。 我试图避免让人们陷入困境,因为我不确定这是否是正确的问题,甚至不确定如何对我正在谈论的内容进行分类。

编辑:检查下面的@aigoncharov解决方案,因为我认为它更快。

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

不知道这是否可以进一步改进。

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

没有评论

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

不知道这是否可以进一步改进。

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

没有评论

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

喜欢这个主意!

可以完成这项工作的另一个技巧是检查两个方向的可分配性。

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: string
}
type B = {
  prop1: string
  prop2: string
}
type C = {
  prop1: string
}

type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>

http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA

@iamandrewluca另一个操场https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA

这里的一个细微差别是Exact<{ prop1: 'a' }>是否应该分配给Exact<{ prop1: string }> 。 在我的用例中,它应该。

@jeremybparagon您的情况已涵盖。 这里还有一些案例。

type InexactType = {
    foo: 'foo'
}

const obj = {
    // here foo is infered as `string`
    // and will error because `string` is not assignable to `"foo"`
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
    foo: 'foo'
}

const obj = {
    // here we cast to `"foo"` type
    // and will not error
    foo: 'foo' as 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

我认为任何使用这个技巧的人(我并不是说它没有有效用途)应该敏锐地意识到在“精确”类型中获得更多道具非常容易。 由于InexactType可以分配给Exact<T, InexactType>如果你有这样的东西,你会在没有意识到的情况下打破准确性:

function test1<T>(t: Exact<T, InexactType>) {}

function test2(t: InexactType) {
  test1(t); // inexactType assigned to exact type
}
test2(obj) // but 

游乐场链接

这就是 TS 没有精确类型的原因(至少是其中之一),因为它需要在精确类型与非精确类型中完全分叉对象类型,其中不精确类型永远不能分配给精确类型,即使从表面上看,它们是兼容的。 不精确的类型可能总是包含更多的属性。 (至少这是@ahejlsberg提到 tsconf 的原因之一)。

如果asExact是标记这样一个精确对象的某种语法方式,那么这样的解决方案可能看起来像这样:

declare const exactMarker: unique symbol 
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

type InexactType = {
    foo: string
}
function asExact<T>(o: T): T & IsExact { 
  return o as T & IsExact;
}

const obj = asExact({
  foo: 'foo',
});


function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {

}

function test2(t: InexactType) {
  test1(t); // error now
}
test2(obj) 
test1(obj);  // ok 

const obj2 = asExact({
  foo: 'foo',
  bar: ""
});
test1(obj2);

const objOpt = asExact < { foo: string, bar?: string }>({
  foo: 'foo',
  bar: ""
});
test1(objOpt);

游乐场链接

@dragomirtitian这就是为什么我更早地提出了解决方案https://github.com/microsoft/TypeScript/issues/12936#issuecomment -524631270 没有受到此影响。

@dragomirtitian这是你如何输入函数的问题。
如果你做的有点不同,它会起作用。

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}

function test2<T extends InexactType>(t: T) {
  test1(t); // fails
}
test2(obj)

https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD + 4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA

@jeremybparagon您的情况已涵盖。

@iamandrewluca我认为这里这里的解决方案对待我的示例的方式上有所不同。

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: 'a'
}
type C = {
  prop1: string
}

type ShouldBeA = Exact<A, C> // This evaluates to never.

const ob...

游乐场链接

@aigoncharov问题是您需要意识到这一点,因此您很容易无法做到这一点,并且test1仍然可以使用额外的属性调用。 IMO 任何可以轻易允许意外不精确分配的解决方案都已经失败,因为重点是强制执行类型系统的准确性。

@toriningen是的,您的解决方案似乎更好,我只是指最后发布的解决方案。 您的解决方案已经证明您不需要额外的函数类型参数,但它似乎不适用于可选属性:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;

const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ? 

游乐场链接

@jeremybparagon我不确定@aigoncharov的解决方案在可选属性上做得很好。 任何基于T extends SS extends T都会受到一个简单的事实的影响,即

type A = { prop1: string }
type C = { prop1: string,  prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y 
type AextendsC = A extends C ? "Y" : "N" // also Y 

游乐场链接

我认为@iamandrewluca使用Exclude<keyof T, keyof Shape> extends never很好,我的类型非常相似(我编辑了我的原始答案以添加&R以确保T extends R无需任何额外检查)。

type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

我不会因为我的解决方案没有漏洞而打赌我的声誉,我对他们来说并没有那么难,但欢迎任何这样的发现😊

我们应该有一个全局启用的标志。 这样,谁想松字就可以继续做下去。 这个问题导致的错误太多了。 现在我尽量避免传播运算符并使用pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

这是我最近偶然发现的确切类型的用例:

type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;

const getFriendlyNameForKey = (key: PossibleKeys) => {
    switch (key) {
        case 'x':
            return 'Ecks';
        case 'y':
            return 'Why';
        case 'z':
            return 'Zee';
    }
};

const myMap: ImmutableMap = { x: 'foo', y: 'bar' };

const renderMap = (map: ImmutableMap) =>
    Object.keys(map).map(key => {
        // Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
        const friendlyName = getFriendlyNameForKey(key);
        // No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.    
        return [friendlyName, map[key]];
    });
;

因为默认情况下类型不精确, Object.keys必须返回string[] (参见 https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208),但在这种情况下,如果ImmutableMap是准确的,它没有理由不能返回PossibleKeys[]

@dallonf请注意,除了精确类型之外,这个示例还需要额外的功能—— Object.keys只是一个函数,需要有某种机制来描述一个函数,该函数返回keyof T的精确类型和string用于其他类型。 仅仅选择声明一个确切的类型是不够的。

@RyanCavanaugh我认为这就是含义,确切的类型 + 检测它们的能力。

反应类型的用例:

forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>

将常规组件传递给forwardRef很诱人,这就是 React 在render参数上检测到propTypesdefaultProps时发出运行时警告的原因。 我们想在类型级别表达这一点,但必须回退到never

- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>

带有never的错误消息没有帮助(“{} 不可分配给 undefined”)。

有人可以帮助我了解@toriningen的解决方案与不同事件对象形状的并集会是什么样子吗? 我想在 redux-dispatch 调用中限制我的事件形状,例如:

type StoreEvent =
  | { type: 'STORE_LOADING' }
  | { type: 'STORE_LOADED'; data: unknown[] }

目前还不清楚我如何制作一个只接受事件的确切形状的类型化 dispatch() 函数。

(更新:我想通了:https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

用例:

缺少Exact<>支持会导致 GraphQL 突变的运行时问题。 GraphQL 接受允许属性的确切列表。 如果您提供过多的道具,则会引发错误。

因此,当我们从表单中获取一些数据时,Typescript 无法验证多余的(额外的)属性。 我们会在运行时得到一个错误。

下面的例子说明了想象中的安全

  • 在第一种情况下跟踪所有输入参数
  • 但在现实生活中(就像我们从表单中获取数据并将其保存到某个变量的第二种情况一样)打字稿

在游乐场试试

Screen Shot 2020-03-05 at 13 04 38

根据文章https://fettblog.eu/typescript-match-the-exact-object-shape/和上面提供的类似解决方案,我们可以使用以下丑陋的解决方案:

Screen Shot 2020-03-05 at 12 26 57

为什么这个savePerson<T>(person: ValidateShape<T, Person>)解决方案是丑陋的?

假设您有深度嵌套的输入类型,例如:

// Assume we are in the ideal world where implemented Exact<>

type Person {
  name: string;
  address: Exact<Address>;
}

type Address {
   city: string
   location: Exact<Location>
}

type Location {
   lon: number;
   lat: number; 
}

savePerson(person: Exact<Person>)

我无法想象我们应该写什么意大利面来获得与当前可用解决方案相同的行为:

savePerson<T, TT, TTT>(person: 
  ValidateShape<T, Person keyof ...🤯...
     ValidateShape<TT, Address keyof ...💩... 
         ValidateShape<TTT, Location keyof ...🤬... 
> > >)

因此,就目前而言,我们的代码中的静态分析存在很大漏洞,它适用于复杂的嵌套输入数据。

第一张图片中描述的情况,由于失去了“新鲜度”,TS 没有验证多余的属性,这对我们来说也是一个痛点。

写作

doSomething({
  /* large object of options */
})

通常感觉比

const options = {
  /* large object of options */
}
doSomething(options)

显式注释const options: DoSomethingOptions = {有所帮助,但它有点麻烦,而且很难在代码审查中发现和执行。

这是一个离题的想法,并不能解决这里描述的大多数用例的准确性,但是当它在封闭范围内只使用一次时,是否有可能保持对象文字新鲜?

@RyanCavanaugh感谢您解释 EPC……EPC 和确切类型之间的区别是否在任何地方进行了更详细的讨论? 现在我觉得我应该更好地理解为什么 EPC 允许某些特定类型不允许的情况。

@noppa我认为这是个好主意。 当我注意到直接分配与首先分配给变量之间的区别时,我偶然发现了这一点 - 甚至问了一个关于 SO

我相信我有与 GraphQL 突变示例相同的问题(确切的嵌套类型,不应允许额外的属性)。 就我而言,我正在考虑在一个通用模块中输入 API 响应(在前端和后端之间共享):

export type ProductsSlashResponse = {
  products: Array<{
    id: number;
    description: string;
  }>,
  total: number;
};

在服务器端,我想确保响应尊重该类型签名:

router.get("products/", async () =>
  assertType<ProductsSlashResponse>(getProducts())));

我已经尝试过这里的解决方案。 一个似乎有效的是T extends U ? U extends T ? T : never : never ,以及一个不理想的柯里化函数。 它的主要问题是你没有得到关于丢失或额外属性的反馈(也许我们可以改进,但是当我们进入嵌套属性时就很难做到)。 其他解决方案不适用于深度嵌套的对象。

当然,如果我发送的信息多于指定的信息,前端通常不会崩溃,但是,如果 API 发送的信息多于应有的信息(并且由于从数据库读取数据的模糊性),这可能会导致信息泄漏哪些类型不一定总是与代码同步,这可能会发生)。

@fer22f GraphQL 不会发送客户端未请求的字段……除非您使用 JSON 标量类型来表示products或数组元素,否则无需担心。

抱歉我看错了,我以为你的意思是你在使用 GraphQL

有人已经提到了 GraphQL,但仅就“收集用例”而言( @DanielRosenwasser几年前在线程中提到 :-) 的“没有现成的用例”),我想要的两个用例使用Exact是:

  1. 将数据传递到数据存储/数据库/ORMs——任何传递的额外字段都将被静默删除/不存储。

  2. 将数据传递到线路调用 / RPC / REST / GraphQL - 再次传递的任何额外字段将被静默删除 / 不发送。

(好吧,也许不是默默丢弃,它们可能是运行时错误。)

在这两种情况下,我都想告诉程序员/我自己(通过编译错误)“......如果你希望它被'存储'或',你真的不应该给我这个额外的属性,b/c发送',它不会是”。

这在“部分更新”样式的 API 中尤其需要,即弱类型:

type Data = { firstName:? string; lastName?: string; children?: [{ ... }] };
const data = { firstName: "a", lastNmeTypo: "b" };
await saveDataToDbOrWireCall(data);

通过弱类型检查 b/c 至少匹配一个参数firstName ,所以它不是 100% 不相交的,但是仍然有一个“明显的”错字lsatNmeTypo没有被发现。

当然,如果我这样做,EPC 可以工作:

await saveDataToDbOrWireCall({ firstName, lastNmeTypo });

但是必须对每个字段进行解构 + 重新键入非常乏味。

@jcalzExactify这样的解决方案适用于 1st-level 属性,但是递归情况(即children是一个数组,数组元素应该是精确的)一旦命中,我就会挣扎带有泛型的“真实世界”用例/如Exact<Foo<Bar<T>>

内置此功能会很棒,并且只是想注意这些显式用例(基本上是具有部分/弱类型的有线调用),如果这有助于优先级/路线图。

(FWIW https://github.com/stephenh/joist-ts/pull/35/files 有我目前在深度ExactExact.test.ts尝试,它正在通过琐碎的情况,但是PR 本身在更深奥的用法上有编译错误。免责声明我真的不希望有人研究这个特定的 PR,但我只是将它提供为“这里Exact会有用”+“AFAICT这在用户空间中很难做到”数据点。)

嘿,

想知道 TS 团队对这里的确切类型记录和元组提案有什么想法? https://github.com/tc39/proposal-record-tuple

为这些新原语引入精确类型有意义吗?

@slorber不是 TS,但这是正交的。 该提议涉及不变性,并且该提议与 Immutable.js 等库之间的担忧几乎相同。

我迭代了@stephenh递归版本。 我在使用递归正确处理未定义的情况时遇到了一些麻烦,我对更清洁的解决方案持开放态度。 它可能不适用于某些具有数组或复杂数据结构的边缘情况。

export type Exact<Expected, Actual> = Expected &
  Actual & // Needed to infer `Actual`
  (null extends Actual
    ? null extends Expected
      ? Actual extends null // If only null stop here, because NonNullable<null> = never
        ? null
        : CheckUndefined<Expected, Actual>
      : never // Actual can be null but not Expected: forbid the field
    : CheckUndefined<Expected, Actual>);

type CheckUndefined<Expected, Actual> = undefined extends Actual
  ? undefined extends Expected
    ? Actual extends undefined // If only undefined stop here, because NonNullable<undefined> = never
      ? undefined
      : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>
    : never // Actual can be undefined but not Expected: forbid the field
  : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>;

type NonNullableExact<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Actual[K] extends (infer ActualElement)[]
      ? Expected[K] extends (infer ExpectedElement)[] | undefined | null
        ? Exact<ExpectedElement, ActualElement>[]
        : never // Not both array
      : Exact<Expected[K], Actual[K]>
    : never; // Forbid extra properties
};

操场

Exact 在返回 API 响应时对我们非常有用。 目前,这是我们的决心:

const response = { companies };

res.json(exact<GetCompaniesResponse, typeof response>(response));
export function exact<S, T>(object: Exact<S, T>) {
  return object;
}

这里Exact类型是@ArnaudBarre上面提供的类型。

感谢@ArnaudBarre解除对我的
提炼你的解决方案:

export type Exact<Expected, Actual> =
  keyof Expected extends keyof Actual
    ? keyof Actual extends keyof Expected
      ? Expected extends ExactElements<Expected, Actual>
        ? Expected
        : never
      : never
    : never;

type ExactElements<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Expected[K] extends Actual[K]
      ? Actual[K] extends Expected[K]
        ? Expected[K]
        : never
      : never
    : never
};

// should succeed (produce exactly the Expected type)
let s1: Exact< { a: number; b: string }, { a: number; b: string } >;
let s2: Exact< { a?: number; b: string }, { a?: number; b: string } >;
let s3: Exact< { a?: number[]; b: string }, { a?: number[]; b: string } >;
let s4: Exact< string, string >;
let s5: Exact< string[], string[] >;
let s6: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string }[] >;

// should fail (produce never)
let f1: Exact< { a: string; b: string }, { a: number; b: string } >;
let f2: Exact< { a: number; b: string }, { a?: number; b: string } >;
let f3: Exact< { a?: number; b: string }, { a: number; b: string } >;
let f4: Exact< { a: number[]; b: string }, { a: string[]; b: string } >;
let f5: Exact< { a?: number[]; b: string }, { a: number[]; b: string } >;
let f6: Exact< { a?: number; b: string; c: string }, { a?: number; b: string } >;
let f7: Exact< { a?: number; b: string }, { a?: number; b: string; c: string } >;
let f8: Exact< { a?: number; b: string; c?: string }, { a?: number; b: string } >;
let f9: Exact< { a?: number; b: string }, { a?: number; b: string; c?: string } >;
let f10: Exact< never, string >;
let f11: Exact< string, never >;
let f12: Exact< string, number >;
let f13: Exact< string[], string >;
let f14: Exact< string, string[] >;
let f15: Exact< string[], number[] >;
let f16: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string } >;

对于 f6、f8 和 f9,先前的解决方案“成功”。
此解决方案还返回“更干净”的结果; 当它匹配时,你会得到“预期”类型。
@ArnaudBarre的评论一样......不确定是否处理了所有边缘情况,所以 ymmv ...

@heystewart你的Exact没有给出对称结果:

let a: Exact< { foo: number }[], { foo: number, bar?: string }[] >;
let b: Exact< { foo: number, bar?: string }[], { foo: number }[] >;

a = [{ foo: 123, bar: 'bar' }]; // error
b = [{ foo: 123, bar: 'bar' }]; // no error

编辑: @ArnaudBarre的版本也有同样的问题

@papb是的,实际上我的输入不起作用是入口点是一个数组。 我需要它用于我们的 graphQL API,其中variables始终是一个对象。

要解决它,您需要隔离 ExactObject 和 ExactArray 并有一个进入其中一个的入口点。

那么确保该对象具有确切属性的最佳方法是什么,不多不少?

@captain-yossarian 说服 TypeScript 团队实现这一点。 此处介绍的解决方案没有适用于所有预期情况,而且几乎所有情况都缺乏清晰度。

@toriningen无法想象如果 TS 团队实现这个功能会关闭多少问题

@瑞安卡瓦诺
目前,我有一个用例将我带到这里,它正好涉及到您的主题“杂项”。 我想要一个函数:

  1. 接受一个参数,该参数实现一个带有可选参数的接口
  2. 返回一个对象类型到给定参数的更窄的实际接口,以便

这些近期目标服务于这些目的:

  1. 我对输入进行了额外的属性检查
  2. 我获得了输出的自动完成和属性类型安全

例子

我已将我的案例简化为:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function y<
    Y extends X
>(
    y: (X extends Y ? Y : X)
) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

该设置有效并实现了所有预期目标,因此从纯粹的可行性角度来看,就目前而言,我很好。 但是,EPC 是通过参数键入(X extends Y ? Y : X) 。 我基本上是偶然发现的,我有点惊讶它的工作原理。

提议

这就是为什么我想要一个implements关键字,它可以用来代替extends以标记此处的类型不应具有多余属性的意图。 像这样:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function x<
    Y implements X
>( y: Y ) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

这对我来说似乎比我目前的解决方法更清楚。 除了更简洁之外,它还使用泛型声明定位整个约束,而不是我目前在泛型和参数之间的拆分。

这也可能使目前不可能或不切实际的进一步用例成为可能,但这目前只是一种直觉。

弱类型检测作为替代方案

值得注意的是,根据 #3842 的弱类型检测也应该解决这个问题,并且可能是有利的,因为不需要额外的语法,如果它与extends ,根据我的用例。

关于Exact<Type>等。

最后,正如我所设想的那样, implements应该非常直接地说明您关于function f<T extends Exact<{ n: number }>(p: T)观点,因为它没有尝试解决更一般的Exact<Type>

一般来说, Exact<Type>在 EPC 旁边似乎没什么用处,我无法想象一个有效的、普遍有用的案例不属于这些组:

  • 函数调用:根据我的示例,这些现在可以轻松处理,并且会受益于implements
  • 赋值:只使用文字,所以 EPC 适用
  • 来自您控制域之外的数据:类型检查不能保护您免受这种情况的影响,您必须在运行时处理它,此时您又回到了安全转换

显然,有些情况下您不能分配文字,但这些也应该是有限集:

  • 如果您在函数中获得赋值数据,请处理调用签名中的类型检查
  • 如果您按照 OP 合并多个对象,则正确断言每个源对象的类型,您就可以安全地转换as DesiredType

总结: implements会很好,但否则我们很好

总之,我相信通过implements和修复 EPC(如果出现问题),确实应该处理确切的类型。

向所有感兴趣的人提问:这里真的有什么东西可以打开吗?

浏览了这里的用例后,我认为现在几乎所有的 repro 都得到了正确的处理,其余的可以用我上面的小例子来处理。 这就引出了一个问题:使用最新的 TS,今天还有人对此有疑问吗?

我对类型注释有一个不成熟的想法。 匹配的一个对象被划分成成员可以完全相等,不多也不少,不多不少,不多不少,不多不少。 对于上述每种情况,都应该有一个表达式。

完全相等,即不多也不少:

function foo(p:{|x:any,y:any|})

//it matched 
foo({x,y})
//no match
foo({x})
foo({y})
foo({x,y,z})
foo({})

更多但不少:

function foo(p:{|x:any,y:any, ...|})

//it matched 
foo({x,y})
foo({x,y,z})

//no matched
foo({x})
foo({y})
foo({x,z})

不多不少:

function foo(p:{x:any,y:any})

//it matched 
foo({x,y})
foo({x})
foo({y})

//no match
foo({x,z})
foo({x,y,z})

或多或少:

function foo(p:{x:any,y:any, ...})

//it matched 
foo({x,y})
foo({x})
foo({y})
foo({x,z})
foo({x,y,z})

结论:

有竖线表示不能少,没有竖线表示可以少。 有省略号意味着可以有更多,没有省略号意味着没有更多。 数组匹配是相同的想法。

function foo(p:[|x,y|]) // p.length === 2
function foo(p:[|x,y, ... |]) // p.length >= 2
function foo(p:[x,y]) // p.length >= 0
function foo(p:[x,y,...]) // p.length >= 0

@rasenplanscher使用您的示例,编译:

const x = { blue: 1, red: 3, purple: 4 };
const z = y(x);

但是对于确切的类型,它不应该。 即这里的要求是不依赖于 EPC。

@xp44mm “更多但不少”已经是行为,如果您将所有属性标记为可选,则“更多或更少”是行为

function foo(p:{x?: any, y?: any}) {}
const x = 1, y = 1, z = 1
// all pass
foo({x,y})
foo({x})
foo({y})
const p1 = {x,z}
foo(p1)
const p2 = {x,y,z}
foo(p2)

类似地,如果我们有精确类型,精确类型 + 所有可选属性本质上就是“不多但少”。

这个问题的另一个例子。 我认为这是对这个提议的一个很好的示范。 在这种情况下,我使用rxjs来处理Subject但想要返回一个(“锁定”) Observable (它没有nexterror等方法来操作价值。)

someMethod(): Observable<MyType> {
  const subject = new Subject<MyType>();

  // This works, but should not. (if this proposal is implemented.)
  return subject;

  // Only Observable should be allowed as return type.
  return subject.asObservable();
}

我总是只想返回确切的类型Observable而不是Subject扩展它。

提议:

// Adding exclamation mark `!` (or something else) to match exact type. (or some other position `method(): !Foo`, ...)
someMethod()!: Observable<MyType> {
  // ...
}

但我相信你有更好的想法。 特别是因为这不仅会影响返回值,对吗? 无论如何,只是一个伪代码演示。 我认为这将是一个很好的功能,可以避免错误和缺失。 就像上面描述的情况一样。 另一种解决方案可能是添加一个新的Utility Type
还是我错过了什么? 这已经有效了吗? 我使用 TypeScript 4。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

MartynasZilinskas picture MartynasZilinskas  ·  3评论

remojansen picture remojansen  ·  3评论

Antony-Jones picture Antony-Jones  ·  3评论

blendsdk picture blendsdk  ·  3评论

weswigham picture weswigham  ·  3评论