如果您在编译时知道如何识别哪些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
为了找回一些安全性,您可以编写一些函数,在将密钥提升到ShopId
或ShopOwnerId
之前对其进行运行时验证.假设有一个通用的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个