对象的类型使函数返回真。

huangapple go评论62阅读模式
英文:

Type of an Object Which Makes Function True

问题

In TypeScript (v4.8),是否有一种方法可以强制执行以下约束:在整个代码库中,我们的库中的开发人员正在引入用户在其自己的代码中将指定的参数。我们希望开发人员以灵活的方式指定可接受值的验证函数,以及在他们没有提供值的情况下的默认值。以下是一些示例:

const a = {validator: (n:number) => n > 10, default: 15}
const b = {validator: (s:string) => s.length < 5, default: 's'}
const c = {validator: (list: Array<number>) => list.pop() === 1, default: [1, 1]}
const d = {validator: (v: boolean) => v, default: true}

default导致validator函数返回false时,我希望它是一种类型错误。

为了了解背景信息:我目前有类似的设置,其中选项必须在数组中明确列出。

type BaseConfigObject = string | boolean | number | object;

interface ConfigOption<F extends BaseConfigObject, D extends F> {
    possible_values: ReadonlyArray<F>;
    default: D;
}

export function checkedConfigOption<F extends BaseConfigObject, D extends F>(value: ConfigOption<F, D>) {
    return value;
}

然后,我们的开发人员编写类似于以下代码:

const terminal_settings = checkedConfigOption({ possible_values: [true, false, 'auto'], default: 'auto' });
const docked_settings = checkedConfigOption({ possible_values: [true, false, 'docked'], default: false });

如果default不是possible_values的成员,将会导致类型错误。这大致相当于具有possible_values.includes(default)的验证函数。

我的问题是 - 是否有一种方法可以在validator(default)为false时引发类型错误,对于任何函数validator和值default

英文:

In TypeScript (v4.8), is there a way to enforce the following constraint: Throughout the codebase our library, our devs are introducing parameters that our end-users will specific in their own code. We would like a flexible way for developers to specify a validation function for acceptable values that the user could supply, as well as a default value in case they don't supply one. Some examples:

const a = {validator: (n:number) =&gt; n &gt; 10, default: 15}
const b = {validator: (s:string) =&gt; string.length &lt; 5, default: &#39;s&#39;}
const c = {validator: (list: Array&lt;number&gt;) =&gt; list.pop() === 1, default = [1, 1]}
const d = {validator: (v: boolean) =&gt; v, default = true}

I would like it to be a type error when the default makes the validator function return false.


For context: I have a similar setup currently, where the options must be explicitly listed in an Array.

type BaseConfigObject = string | boolean | number | object;

interface ConfigOption&lt;F extends BaseConfigObject, D extends F&gt; {
    possible_values: ReadonlyArray&lt;F&gt;;
    default: D;
}

export function checkedConfigOption&lt;F extends BaseConfigObject, D extends F&gt;(value: ConfigOption&lt;F, D&gt;) {
    return value;
}

Our devs then write things like:

const terminal_settings = checkedConfigOption({ possible_values: [true, false, &#39;auto&#39;], default: &#39;auto&#39; });
const docked_settings = checkedConfigOption({ possible_values: [true, false, &#39;docked&#39;], default: false });

This leads to a type error if the default is not a member of the possible_values. This is roughly the equivalent of having a validation function of possible_values.includes(default).

My question is - is there a way to cause a type error when validator(default) is false, for any function validator and value default?

答案1

得分: 1

这是不幸的不可能的。在TypeScript的类型系统中,除非函数是泛型或重载的,否则函数的返回类型与其任何参数的类型无关。所以在这个例子中:

const a = { validator: (n: number) => n > 10, default: 15 }
const b = { validator: (s: string) => s.length < 5, default: 's' }
const c = { validator: (list: Array<number>) => list.pop() === 1, default: [1, 1] }
const d = { validator: (v: boolean) => v, default: true }

validator 属性都是返回 boolean 类型的函数,无论输入值如何。虽然 boolean 等同于 联合类型 true | false,但这只意味着每次调用函数都返回“要么 true 要么 false”。TypeScript 不知道哪些输入值返回 true,哪些返回 false。这对于人类开发者来说可能很明显,但编译器不会通过模拟不同的假设输入来分析函数体的行为;这将非常昂贵,几乎无法编译任何复杂的程序。在某种意义上,你期望编译器实际上在编译时运行 x.validator(x.default),但事实并非如此。

不幸的是,这是运行时验证的工作,而不是编译时验证。

我花了一点精力思考你能够实现多接近的方式。如上所述,输出类型与输入类型无关,除非函数是泛型或重载的。

重载函数是死胡同,因为目前对于重载函数的推断只考虑最后一个调用签名(参见 ms/TS#43187 获取更多信息)。

泛型在大多数情况下也是死胡同,因为要抽象泛型将需要高种类类型的支持,如 ms/TS#1213 中所述,而 TypeScript 不直接支持它。你可以模拟支持它,所以我使用了 free-types 来尝试,结果... 很糟糕:

import { Type, A, apply } from 'free-types';

type SplitStr<T extends string, A extends string[] = []> = T extends `${infer F}${infer R}` ? SplitStr<R, [...A, F]> : A;
type BValidator<T extends string> = "5" extends keyof SplitStr<T> ? false : true;

interface $BValidator extends Type<[string]> {
  type: BValidator<A<this>>
}

function bValidator<T extends string>(s: string): BValidator<T> {
  return s.length < 5 as any;
}

type Validator<$T extends Type<[any]>, V extends apply<$T, [V]> extends true ? any : never> = {
  validator: <U extends ($T extends Type<[infer T]> ? T : never) >(u: U) => apply<$T, [U]>,
  default: V
}

const asValidator = <$T extends Type<[any]>>(
  validator: <U extends ($T extends Type<[infer T]> ? T : never) >(
    u: U) => apply<$T, [U]>) =>
  <V extends ($T extends Type<[infer T]> ? T : never) & (
    apply<$T, [V]> extends true ? any : never)>(
      d: V): Validator<$T, V> => (
    { validator, default: d });

const bVal = asValidator<$BValidator>(bValidator);
const bGood = bVal("s"); // okay
// const bGood: Validator<$BValidator, "s">
bGood.validator(bGood.default); // okay
const bBad = bVal("oopsiedoodle") // error
// const bBad: Validator<$BValidator, never>

我设法编码了字符串 字面类型 是否少于五个字符,并将 bValidator 编码为一个在类型级别和运行时执行此操作的泛型函数... 但编译器无法验证这种等价性,所以我需要一个类型断言。

然后我可以使用 asValidator(bValidator) 构建验证器,但编译器无法推断出高阶泛型类型,因此我们需要通过 asValidator<$BValidator>(bValidator) 指定它,然后在所有这些之后,我们才能用 &quot;s&quot; 初始化 b,但不能用 &quot;oopsiedoodle&quot;

太繁琐了。所以对于每个验证函数,你需要费力地尝试在类型级别表示有效和无效输入之间的区别,然后断言验证函数实际上是这样的,然后描述适合它的高阶泛型类型... 糟糕,不感兴趣。

与之相比,看看等效的运行时版本:

interface Validator<T> {
  validator: (t: T) => boolean;
  default: T
}
const asValidator = <T,>(validator: (t: T) => boolean, def: T) => {
  if (!validator(def)) throw new Error("YOU MESSED UP!!");
  return { validator, default: def };
}
const bGood = 
  asValidator((s: string) => s.length < 5, "s"); // okay
const bBad = 
  asValidator((s: string) => s.length < 5, "oopsiedoodle"); // YOU MESSED UP!!

这好了一百万倍。所以总之,这是运行时验证的工作,而不是编译时验证,不幸的是。

英文:

This is unfortunately impossible. In TypeScript's type system, unless a function is generic or overloaded, the return type of the function is independent of the types of any of its parameters. So inside

const a = { validator: (n: number) =&gt; n &gt; 10, default: 15 }
const b = { validator: (s: string) =&gt; s.length &lt; 5, default: &#39;s&#39; }
const c = { validator: (list: Array&lt;number&gt;) =&gt; list.pop() === 1, default: [1, 1] }
const d = { validator: (v: boolean) =&gt; v, default: true }

the validator properties are all functions that return boolean regardless of the input value. And although boolean is equivalent to the union type true | false, that just means that every call to the function returns "either true or false". TypeScript has no idea which input values return true and which ones return false. It might seem obvious to a human developer how to determine that, but the compiler does not analyze function bodies by simulating their behavior for different hypothetical inputs; it would be unbelievably expensive to do so, and it would be impossible to compile almost any non-trivial program. In some sense, you're expecting the compiler to actually run x.validator(x.default) at compile time, and that's just not how it works.

This is the job of runtime validation, not compile time validation, unfortunately.


I spent a little effort wondering how close you could get, though. As mentioned above, the output type is independent of the input types, unless the function is generic or overloaded.

Overloaded functions are a dead end, because currently inference with overloaded functions only looks at the last call signature (see ms/TS#43187 for more information).

Generics are mostly a dead end because abstracting over generics would require higher kinded types as described in ms/TS#1213, and TypeScript doesn't have direct support for it. You can kind of emulate support for it, so I used free-types to try, and, well... it's bad:

import { Type, A, apply } from &#39;free-types&#39;
type SplitStr&lt;T extends string, A extends string[] = []&gt; = T extends `${infer F}${infer R}` ? SplitStr&lt;R, [...A, F]&gt; : A;
type BValidator&lt;T extends string&gt; = &quot;5&quot; extends keyof SplitStr&lt;T&gt; ? false : true;
interface $BValidator extends Type&lt;[string]&gt; {
type: BValidator&lt;A&lt;this&gt;&gt;
}
function bValidator&lt;T extends string&gt;(s: string): BValidator&lt;T&gt; {
return s.length &lt; 5 as any;
}
type Validator&lt;$T extends Type&lt;[any]&gt;, V extends apply&lt;$T, [V]&gt; extends true ? any : never&gt; = {
validator: &lt;U extends ($T extends Type&lt;[infer T]&gt; ? T : never) &gt;(u: U) =&gt; apply&lt;$T, [U]&gt;,
default: V
}
const asValidator = &lt;$T extends Type&lt;[any]&gt;&gt;(
validator: &lt;U extends ($T extends Type&lt;[infer T]&gt; ? T : never) &gt;(
u: U) =&gt; apply&lt;$T, [U]&gt;) =&gt;
&lt;V extends ($T extends Type&lt;[infer T]&gt; ? T : never) &amp; (
apply&lt;$T, [V]&gt; extends true ? any : never)&gt;(
d: V): Validator&lt;$T, V&gt; =&gt; (
{ validator, default: d });
const bVal = asValidator&lt;$BValidator&gt;(bValidator);
const bGood = bVal(&quot;s&quot;); // okay
// const bGood: Validator&lt;$BValidator, &quot;s&quot;&gt;
bGood.validator(bGood.default); // okay
const bBad = bVal(&quot;oopsiedoodle&quot;) // error
// const bBad: Validator&lt;$BValidator, never&gt;

I managed to encode whether a string literal type has less than five characters, and then encode bValidator as a generic function that performs this operation at the type level as well as at runtime... but the compiler cannot verify that equivalence so I needed a type assertion.

And then I can build up a validator using asValidator(bValidator), but the compiler can't infer the higher-order generic so we need to specify it via asValidator&lt;$BValidator&gt;(bValidator), and then after all that, we are allowed to initialize b with a default of &quot;s&quot; but not with a default of &quot;oopsiedoodle&quot;.

Blegggh. So for each validator function you'd need to painstakingly try to represent the distinction between valid and invalid inputs at the type level, and then assert that the validator function actually does that, and then describe the higher order generic type that fits it, and... yuck, no thanks.


Instead compare to the equivalent runtime version:

interface Validator&lt;T&gt; {
validator: (t: T) =&gt; boolean;
default: T
}
const asValidator = &lt;T,&gt;(validator: (t: T) =&gt; boolean, def: T) =&gt; {
if (!validator(def)) throw new Error(&quot;YOU MESSED UP!!&quot;);
return { validator, default: def };
}
const bGood = 
asValidator((s: string) =&gt; s.length &lt; 5, &quot;s&quot;); // okay
const bBad = 
asValidator((s: string) =&gt; s.length &lt; 5, &quot;oopsiedoodle&quot;); // YOU MESSED UP!!

This is a million times better. So, in conclusion, this is the job of runtime validation, not compile time validation, unfortunately.

Playground link to code

huangapple
  • 本文由 发表于 2023年3月31日 03:50:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/75892409.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定