typescript - 类型映射时通用属性的问题

标签 typescript generics

我有一个库,它导出类似于以下的实用程序类型:

type Action<Model extends object> = (data: State<Model>) => State<Model>;

此实用程序类型允许您声明将作为“操作”执行的函数。它接收一个通用参数是 Model行动将针对。
data然后使用我导出的另一种实用程序类型输入“ Action ”的参数;

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
State实用型基本上取传入Model generic 然后创建一个新类型,其中所有属于 Action 类型的属性。已被删除。

例如这是上述的基本用户土地实现;

interface MyModel {
  counter: number;
  increment: Action<Model>;
}

const myModel = {
  counter: 0,
  increment: (data) => {
    data.counter; // Exists and typed as `number`
    data.increment; // Does not exist, as stripped off by State utility 
    return data;
  }
}

上述工作非常好。 👍

但是,有一种情况我很挣扎,特别是在定义了泛型模型定义以及用于生成泛型模型实例的工厂函数时。

例如;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

在上面的例子中,我期望 data要在 doSomething 处输入的参数操作已被删除,通用 value属性(property)仍然存在。然而事实并非如此 - value我们的 State 也删除了属性(property)公用事业。

我相信造成这种情况的原因是 T是泛型的,没有对其应用任何类型限制/缩小,因此类型系统决定它与 Action 相交。键入并随后将其从 data 中删除参数类型。

有没有办法绕过这个限制?我做了一些研究,希望有某种机制可以说明 T是除 Action 之外的任何一种.即否定类型限制。

想象:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {

但是 TypeScript 不存在该功能。

有谁知道我可以按照我的预期让它工作的方法吗?

为了帮助调试,这里有一个完整的代码片段:

// Returns the keys of an object that match the given type(s)
type KeysOfType<A extends object, B> = {
  [K in keyof A]-?: A[K] extends B ? K : never
}[keyof A];

// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;

// My utility function.
type Action<Model extends object> = (data: State<Model>) => State<Model>;

interface MyModel<T> {
  value: T; // 👈 a generic property
  doSomething: Action<MyModel<T>>;
}

function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    doSomething: data => {
      data.value; // Does not exist 😭
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

您可以在此处使用此代码示例:
https://codesandbox.io/s/reverent-star-m4sdb?fontsize=14

最佳答案

这是一个有趣的问题。对于条件类型中的泛型类型参数,Typescript 通常不能做太多事情。它只是推迟对 extends 的任何评估如果它发现评估涉及一个类型参数。

如果我们可以让 typescript 使用一种特殊的类型关系,即 ,则异常(exception)情况适用。相等关系 (不是扩展关系)。对于编译器来说,相等关系很容易理解,因此不需要推迟条件类型评估。泛型约束是编译器中为数不多的使用类型相等的地方之一。让我们看一个例子:

function m<T, K>() {
  type Bad = T extends T ? "YES" : "NO" // unresolvable in ts, still T extends T ? "YES" : "NO"

  // Generic type constrains are compared using type equality, so this can be resolved inside the function 
  type Good = (<U extends T>() => U) extends (<U extends T>() => U) ? "YES" : "NO" // "YES"

  // If the types are not equal it is still un-resolvable, as K may still be the same as T
  type Meh = (<U extends T>()=> U) extends (<U extends K>()=> U) ? "YES": "NO" 
}

Playground Link

我们可以利用这种行为来识别特定类型。现在,这将是精确类型匹配,而不是扩展匹配,并且精确类型匹配并不总是合适的。但是,由于 Action只是一个函数签名,精确的类型匹配可能足够好。

让我们看看我们是否可以提取匹配更简单的函数签名的类型,例如 (v: T) => void :
interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]: Identical<M[K], (v: T) => void, never, K>
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: Identical<T, (v: T) => void, never, "value">;
  //     other: "other";
  //     action: never;
  // }

}

Playground Link

以上类型KeysOfIdenticalType接近我们过滤所需的内容。对于 other ,属性名称被保留。对于 action ,属性名称被删除。 value周围只有一个讨厌的问题.自 value类型为 T , 不是很容易解决的 T , 和 (v: T) => void不相同(实际上它们可能不相同)。

我们仍然可以确定valueT 相同:对于 T 类型的属性,与此检查相交 (v: T) => voidnever .与 never 的任何交集可以简单地解析为 never .然后我们可以添加回 T 类型的属性。使用另一个身份检查:
interface Model<T> {
  value: T,
  other: string
  action: (v: T) => void
}

type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

function m<T>() {
  type M = Model<T>
  type KeysOfIdenticalType = {
    [K in keyof M]:
      (Identical<M[K], (v: T) => void, never, K> & Identical<M[K], T, never, K>) // Identical<M[K], T, never, K> will be never is the type is T and this whole line will evaluate to never
      | Identical<M[K], T, K, never> // add back any properties of type T
  }
  // Resolved to
  // type KeysOfIdenticalType = {
  //     value: "value";
  //     other: "other";
  //     action: never;
  // }

}

Playground Link

最终的解决方案如下所示:
// Filters out an object, removing any key/values that are of Action<any> type
type State<Model extends object, G = unknown> = Pick<Model, {
    [P in keyof Model]:
      (Identical<Model[P], Action<Model, G>, never, P> & Identical<Model[P], G, never, P>)
    | Identical<Model[P], G, P, never>
  }[keyof Model]>;

// My utility function.
type Action<Model extends object, G = unknown> = (data: State<Model, G>) => State<Model, G>;


type Identical<T, TTest, TTrue, TFalse> =
  ((<U extends T>(o: U) => void) extends (<U extends TTest>(o: U) => void) ? TTrue : TFalse);

interface MyModel<T> {
  value: T; // 👈 a generic property
  str: string;
  doSomething: Action<MyModel<T>, T>;
  method() : void
}


function modelFactory<T>(value: T): MyModel<T> {
  return {
    value,
    str: "",
    method() {

    },
    doSomething: data => {
      data.value; // ok
      data.str //ok
      data.method() // ok 
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

/// Still works for simple types
interface MyModelSimple {
  value: string; 
  str: string;
  doSomething: Action<MyModelSimple>;
}


function modelFactory2(value: string): MyModelSimple {
  return {
    value,
    str: "",
    doSomething: data => {
      data.value; // Ok
      data.str
      data.doSomething; // Does not exist 👍
      return data;
    }
  };
}

Playground Link

备注:这里的限制是这只适用于一种类型参数(尽管它可能适用于更多)。此外,API 对任何消费者来说都有些困惑,因此这可能不是最佳解决方案。可能存在我尚未确定的问题。如果你找到了,请告诉我😊

关于typescript - 类型映射时通用属性的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58738700/

相关文章:

ios - 接受类型序列的函数

angular - 循环遍历 Typescript 中的 observable<Object[ ]>

javascript - Angular 4专注于向下箭头上的元素并滚动

javascript - Storybook js 中未找到规则 '@typescript-eslint/no-implicit-any' 的定义

java - 在类声明中定义 Java 泛型类型

java - 如何模拟接受类类型的泛型方法?

typescript - 在 LitElement 项目中使用 @rollup/plugin-image 输出 svgs 以与 RollupJS 捆绑

javascript - Window 对象是否有获取/设置键的方法?

swift - swift 的通用下标

swift - `func<T: Type>` 和 `func<T>(..) where T: Type` 之间的区别?