这可能是一个简单的问题,但我一直无法找到解决方案.

我有一本词典,里面有不同类型的值.我想以这样一种方式键入词典,即TypeScrip根据键的类型知道值的类型.

以下是设置:

type Id = string;

interface Shop {
  id: Id;
  a: ... // `a` is a property unique to Shop
}

interface ShopOwner {
  id: Id;
  b: ... // `b` is a property unique to ShopOwner
}

type Objects = {[key: Id]: Shop | ShopOwner }

// when I call the dictionary with a key that is guaranteed to return ShopOwner,
// typescript throws an error saying returned value can be either object ShopOwner or Shop
// and Shop doesn't have property b 
const shopOwner = dictionary[key].b 

我可以在访问b之前判断值的类型,但我觉得使用泛型有一个很好的解决方案.一些类似的事情

type Id<T is either Shop or ShopOwner> = ?
type Objects<T is either Shop or ShopOwner> = {[key: Id<T>]: T}

我怎么能实现这样的事情呢?

谢谢!

推荐答案

如果您在编译时知道如何识别哪些id对应于Shop,哪些对应于ShopOwner,那么事情就容易多了.例如,如果Shop个ID以字符串"shop_"开头,而ShopOwner个ID以字符串"owner_"开头,则可以使用template literal types来跟踪这一区别:

type ShopId = `shop_${string}`;
type ShopOwnerId = `owner_${string}`;

interface ShopOwner {
  id: ShopOwnerId;
  name: string;
  shop: Shop;
}

interface Shop {
  id: ShopId;
  name: string;
  address: string;
}

dataNormalized表将是Record object types中的an intersection个,其中ShopId个键与Shop个值相匹配,而ShopOwnerId个键与ShopOwner个值相匹配:

declare const dataNomalized: Record<ShopId, Shop> & Record<ShopOwnerId, ShopOwner>;

你的getEntity()可以很容易地写成

function getEntity<I extends ShopId | ShopOwnerId>(id: I) {
  const ret = dataNomalized[id];
  if (!ret) throw new Error("no entry at '" + id + "'")
  return ret;
}

您将获得有效的运行时和编译时行为:

console.log(getEntity("shop_b").address.toUpperCase());
console.log(getEntity("owner_a").shop.address.toUpperCase());
getEntity("who knows") // compiler error, 
// 'string' is not assingbale to '`shop_${string}` | `owner_${string}`'

不幸的是,你不知道哪ids对应Shop,哪ids对应ShopOwner……或者至少编译器不知道.它们都是string,编译器很难区分它们.


在这种情况下,您可以做的最好的是pretend,有一些这样的区别.本例中使用的一种技术是将"brand" a primitive type设置为string,并使用一个标签说明它的用途:

type Id<K extends string> = string & { __type: K }
type ShopId = Id<"Shop">;
type ShopOwnerId = Id<"ShopOwner">;

所以这里的ShopId应该是string,它的属性也是__type,literal type,"Shop".和ShopOwnerId类似,只是它的__type属性是"ShopOwner".如果这是真的,您只需判断__type属性并相应地执行操作即可.但当然,在运行时,您只有string,您不能向它们添加属性.不,这只是我们对编译器说的一个方便的谎言.

你们有相似的接口定义,

interface ShopOwner {
  id: ShopOwnerId;
  name: string;
  shop: Shop;
}

interface Shop {
  id: ShopId;
  name: string;
  address: string;
}

并且您的dataNormalized仍然是类型Record<ShopId, Shop> & Record<ShopOwnerId, ShopOwner>,但现在您必须确保您的id是适当类型的,因为编译器无法验证它:

const owner = {
  id: "a",
  name: "Peter",
  shop: {
    id: "b",
    name: "Peters's Shop",
    address: "123 Main St"
  }
} as ShopOwner; // assert

const dataNomalized = {
  a: owner,
  b: owner.shop,
} as Record<ShopId, Shop> & Record<ShopOwnerId, ShopOwner>; // assert

虽然getEntity()可以以相同的方式实现,但您会发现很难直接使用它:

getEntity("a") // compiler error!
getEntity("b") // compiler error!
//Argument of type 'string' is not assignable to parameter of type 'ShopId | ShopOwnerId'.

您可以继续并继续断言这些密钥的类型是正确的,但您可能会断言错误的内容:

getEntity("a" as ShopId).address.toUpperCase(); // okay for the compiler, but
// runtime error: ???? getEntity(...).address is undefined 

为了找回一些安全性,您可以编写一些函数,在将密钥提升到ShopIdShopOwnerId之前对其进行运行时验证.假设有一个通用的custom type guard function,以及每个特定id类型的一些帮助器:

function isValidId<K extends string>(x: string, type: K): x is Id<K> {
  // do any runtime validation you want to do here, if you care
  if (!(x in dataNomalized)) return false;
  const checkKey: string | undefined = ({ Shop: "address", ShopOwner: "shop" } as any)[type];
  if (!checkKey) return false;
  if (!(checkKey in (dataNomalized as any)[x])) return false;
  return true;
}

function shopId(x: string): ShopId {
  if (!isValidId(x, "Shop")) throw new Error("Invalid shop id '" + x + "'");
  return x
}

function shopOwnerId(x: string): ShopOwnerId {
  if (!isValidId(x, "ShopOwner")) throw new Error("Invalid shop id '" + x + "'");
  return x;
}

现在,您可以安全地编写代码:

console.log(getEntity(shopId("b")).address.toUpperCase());
console.log(getEntity(shopOwnerId("a")).shop.address.toUpperCase());

那么,当您犯错误时,仍然会收到运行时错误,但至少它们会被尽可能早地发现:

getEntity(shopId("who knows")).address.toUpperCase(); // Invalid shop id 'who knows' 

老实说,这不是great.理想情况下,您应该重构,以便在编译时可以区分您的ID.但如果不这样做,您至少可以使用标记原语来帮助您保持条理清晰,即使它们不太能保证,因为在运行时没有这样的区别.

Playground link to code

Typescript相关问答推荐

如何在单击停止录制按钮后停用摄像机?(使用React.js和Reaction-Media-Recorder)

类型脚本满足将布尔类型转换为文字.这正常吗?

如何在NX工作区项目.json中设置类似angular.json的两个项目构建配置?

(TypeScript)TypeError:无法读取未定义的属性(正在读取映射)"

如何在React组件中指定forwardRef是可选的?

TypeScrip-根据一个参数值验证另一个参数值

在抽象类属性定义中扩展泛型类型

如何在打字角react 式中补齐表格组

如何正确地将元组表格输入到对象数组实用函数(如ZIP)中,避免在TypeScrip 5.2.2中合并所有值

Cypress-验证别名的存在或将别名中的文本与';if';语句中的字符串进行比较

内联类型断言的工作原理类似于TypeScrip中的断言函数?

TS不能自己推断函数返回类型

如何通过nx找到所有受影响的项目?

从 npm 切换到 pnpm 后出现Typescript 错误

TS 条件类型无法识别条件参数

从函数参数推断函数返回类型

有效索引元组的Typescript 类型?

在 Typescript 中,如何修改使用通用键获得的数组类型的属性?

如何重新映射函数参数的对象类型以生成所需的类型形状

Shadcn ui 可以使用 javascript 而不是 typescript 为 Vite + React 安装吗?