Flowtype:泛型 Id<T> 类型,具有与传入的类型参数类似的约束

Flowtype: generic Id<T> type with similar constraints to the type argument passed in

我有一个通用的 Id<T: HasId> 类型,它在结构上始终只是一个 string,无论作为 T 传入的类型参数如何。我希望 Id<T> 类型与作为 T 传递的不同类型表现得像不同类型。

例如,我希望以下代码中的代码段 const i :Id<Car> = p.id 导致流错误:

declare interface HasId {
  id: string,
};

type Id<T: HasId> = string;

type Person = {
  id: Id<Person>,
  name: string,
};

type Car = {
  id: Id<Car>,
  make: string,
  model: string,
};

const p :Person = { id: '1234', name: 'me' }

const c :Car = p; // Causes a Flow error, good!

const c :Id<Car> = p.id; // I want this to cause a Flow error,
                         //   but currently it doesn't.

此外,如果这可以继续很好地与联合类型一起工作,那就太好了:

type Vehicle =
  | Car
  | Motorcycle
;

const t :Car = { id: '5678', make: 'Toyota', model: 'Prius' };

const v :Id<Vehicle> = c.id; // Currently does not cause Flow
                             //   error; I want to keep it that way.

听起来你想要的是不透明类型,而 Flow 还没有。如果您有类型别名 type MyString = string,则可以互换使用 stringMyString。但是,如果您有一个不透明的类型别名 opaquetype MyNumber = number,则不能互换使用 numberMyNumber

不透明类型有更长的解释 on this GitHub issue

相当不错但有点老套的解决方案

我做了一些实验,并找到了一种方法来根据 this GitHub issue comment and the one following it 中所示的系统来完成我在问题中指定的内容。您可以使用 Flow 处理不透明类型的 class(带有通用类型参数 T),并使用转换为 any 以便在字符串和 ID 之间进行转换。

这里有一些实用程序可以实现这一点:

// @flow

import { v4 } from 'node-uuid';

// Performs a "type-cast" from string to Id<T> as far as Flow is concerned,
//   but this is a no-op function
export function stringToId<T>(s :string):Id<T> {
  return (s :any);
}

// Use this when you want to treat the ID as a string without a Flow error
export function idToString(i :Id<*>):string {
  return (i :any);
}
export function createId<T>():Id<T> {
  return stringToId('1234');
}

// Even though all IDs are strings, this type distinguishes between IDs that
//   can point to different objects.
export class Id<T> {};

使用这些实用程序,以下代码(类似于我问题中的原始代码)将导致 Flow 错误,就像我想要的那样。

// @flow
const p :Id<Person> = createId<Person>();
// note: Even though p is Id<Person> in Flow, its actual runtime type is string.

const c :Id<Car> = p; // this causes Flow errors. Yay!

// Also works without an explicit annotation for `p`:
const pp = createId<Person>();
const cc :Id<Car> = pp; // also causes Flow errors. Yay!

缺点

不幸的是,Flow 输出非常冗长,因为像这样的类型错误会触发多个 Flow 错误。尽管输出不理想,但至少它的行为是正确的,因为出错会导致 Flow 报告错误。

另一个问题是,使用此解决方案,在 Flow 不期望 Id<*> 的 object/map 键等情况下,您必须显式地将 ID 转换为字符串,例如:

// @flow
type People = { [key :string]: Person };

const people :People = {};

const p :Id<Person> = createId<Person>();

people[p] = createPerson(); // causes Flow error
// Unfortunately you always have to do this:
people[idToString(p)] = createPerson(); // no Flow error

这些类型转换函数在运行时只是空操作,因为所有 Flow 类型都被剥离了,所以如果你经常调用它们可能会降低性能。请参阅我在此答案中链接的 GitHub 问题以进行更多讨论。

注意:我使用的是 Flow v0.30.0。