Typescript 下 Mongoose 外键类型&外键数组类型定义&类型保护&联合类型理解

最近给项目上Typescript,记录在迁移的过程中遇到的一个问题。

问题背景

下面这段代码 定义了一个User 接口, Company接口, Order接口以及相应的mongoose model。 User有一个外键关联的Company,和很多外键关联的Orders。

interface IUser & Document{
  _id: string;
  company: string | ICompany;
  orders: string[] | IOrder[];
}

interface ICompany & Document{
  _id: string;
  name: string;
}

interface IOrder & Document{
  _id: string
  title: string;
}

const userSchema: Schema = new Schema({
  company: { type: ObjectId, ref: 'Company' },
  orders: [{ type: ObjectId, ref: 'Order' }]
});

const companySchema: Schema = new Schema({
  name: String
});

const orderSchema: Schema = new Schema({
  title: String
});

export const User: Model<IUser> = mongoose.model('User', userSchema);
const Company: Model<ICompany> = mongoose.model('Company', companySchema);
const Order: Model<IOrder> = mongoose.model('User', orderSchema);
复制代码

问题重现

使用时可能遇到如下场景:

import { User } from 'models'
User.findOne()
.populate({ 'path': 'company' })
.populate({ 'path': 'orders'})
.then(user => {
    // 在此处尝试访问直接populate 出来的 company object的属性,会遇到编译器报错
    // Property '_id' does not exist on type 'string | ICompany'.
    // Property '_id' does not exist on type 'string'.
    const companyId = user.company._id

    // 在此处尝试访问order.map,会遇到编译器报错
    // Cannot invoke an expression whose type lacks a call signature. 
    // '(<U>(callbackfn: (value: string, index: number, array: string[]) => U, thisArg?: any) => U[]) 
    // | (<U>(callbackfn: (value: TOrder, index: number, array: TOrder[]) => U, thisArg?: any) => U[])'
    // has no compatible call signatures.ts(2349)
    user.orders.map(order => order)
})

复制代码

这两个问题都涉及到了对联合类型的理解的问题。在写这两行代码的时候我理所应当的认为联合类型就是 或(or)的意思。 即,取决于是否调用populate方法:

  • company 可以是 string 或者 Company对象
  • orders 可以使 string数组或者 Order对象的数组

那么理所应当user.company._id 和 user.orders.map 都应该可以直接调用而没有问题的。

问题原因

那么编译器为什么会报错呢? 仔细读了一下文档发现:

如果一个值是联合类型(Union Types),我们只能访问此联合类型的所有类型里共有的成员。

这里的联合类型并不是并集的意思,而可以理解为交集。

  • string没有string._id。编译器报错。

而第二个问题,则比较复杂而且一直有人在提解决方案,后面再说一下。

问题解决

那么这种情况改怎么解决呢?

类型断言/重载 type assertion

对于第一个问题,其实当时想到了一个方法就是用Type Assertion

const company = user.company
const companyId = (company as ICompany)._id
复制代码

这样编译器就不会报错了,虽然能解决问题,但其实并不是一个很安全的操作。需要在写代码的时候清晰的搞清楚什么时候populate了,什么时候没有populate。(虽然看起来很简单但这其实是增加了一个human error的机会)

所以又去读了读文档发现了:

自定义类型保护 (type guard) 利用typescript的自定义类型保护,这样编译器就能确认类型从而不会报错了。

function isCompany(obj: string | ICompany): obj is Company {
    return obj && obj._id;
}

import { User } from 'models'
User.findOne()
.populate({ 'path': 'company' })
.populate({ 'path': 'orders'})
.then(user => {
    if(isCompany(user.company)
      const companyId = user.company._id
    // ...
})
复制代码

数组类型定义使用混合类型

但是第二个问题就有点无语,两个类型都是数组,怎么连map都调用不了?

先给一个解决方案吧:

interface IUser & Document{
  // 原来的声明 orders: string[] | IOrder[];
  orders: (string | IOrder)[]
}
复制代码

这样编译器问题可以解决了,但是实际上这个声明会允许['id', order]这样的混合数组存在。我们需要的是要么 ['id','id'], 要么[order, order].

在github上这个issue也被提了很多次,最早可以追溯到2016年,并且最近依旧在不断被提起,感兴趣的可以去看一下:Call signatures of union types

另外,11天前有人开了一个新issue Wishlist: support for correlated record types 希望能解决这个问题。

Typescript语言的开发者Ryan也针对开发者jcalz的在最新的一个issue里评论了。

会持续关注这个问题。

转载于:https://juejin.im/post/5ca7ffd2f265da30cf176f60

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值