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

ctr*_*usb 11 generics typescript

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

type Action<Model extends object> = (data: State<Model>) => State<Model>;
Run Code Online (Sandbox Code Playgroud)

此实用程序类型使您可以声明一个将作为“动作”执行的功能。它收到一个通用的论据,Model即该操作将针对的论点。

data然后使用导出的另一个实用程序类型键入“ action” 的参数。

type State<Model extends object> = Omit<Model, KeysOfType<Model, Action<any>>>;
Run Code Online (Sandbox Code Playgroud)

State实用型基本上接受了输入Model通用,然后创建一个新的类型,其中所有的类型的属性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;
  }
}
Run Code Online (Sandbox Code Playgroud)

以上效果很好。

但是,在某些情况下,特别是在定义了通用模型定义以及工厂函数以产生通用模型实例的情况下,我很费劲。

例如;

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;
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,我希望在datadoSomething删除操作的地方键入自变量,并且通用value属性仍然存在。但是,情况并非如此-该value属性也已被我们的State实用程序删除。

我相信这是因为它T是通用的,没有对其施加任何类型限制/缩小,因此类型系统决定它与一个Action类型相交,然后将其从data参数类型中删除。

有办法解决这个限制吗?我已经做了一些研究,希望能有一些机制可以说明除之外T任何机制。即否定类型限制。Action

想像:

function modelFactory<T extends any except Action<any>>(value: T): UserDefinedModel<T> {
Run Code Online (Sandbox Code Playgroud)

但是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;
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

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

Tit*_*mir 7

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

如果我们可以让打字稿使用一种特殊的类型关系,即相等关系(而不是扩展关系),则适用例外。对于编译器而言,相等关系很容易理解,因此无需推迟条件类型评估。通用约束是编译器中使用类型相等的少数几个位置之一。让我们看一个例子:

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" 
}
Run Code Online (Sandbox Code Playgroud)

游乐场链接

我们可以利用这种行为来识别特定类型。现在,这将是完全类型匹配,而不是扩展匹配,并且完全类型匹配并不总是适合。但是,由于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;
  // }

}
Run Code Online (Sandbox Code Playgroud)

游乐场链接

上面的类型KeysOfIdenticalType很接近我们需要过滤的类型。对于other,保留属性名称。对于action,属性名称被删除。周围只有一个讨厌的问题value。由于value类型为T,所以不能简单地解析T,并且(v: T) => void也不相同(实际上它们可能不是)。

我们仍然可以确定value与相同T:对于type的属性T,将此检查(v: T) => void与相交never。与的任何交集都可以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;
  // }

}
Run Code Online (Sandbox Code Playgroud)

游乐场链接

最终的解决方案如下所示:

// 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;
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

游乐场链接

注意:这里的限制是,它仅适用于一个类型参数(尽管它可能适用于更多类型参数)。而且,该API对于任何使用者而言都有些混乱,因此这可能不是最佳解决方案。可能有一些我尚未发现的问题。如果发现任何问题,请通知我

  • 我觉得白人刚达夫刚刚露面了。TBH我已经准备将其作为编译器的限制而被注销。所以很高兴尝试一下。谢谢! (2认同)