首页 > 解决方案 > 在基于超类型对象创建子类型对象时,如何确保我仅手动提供缺少的属性?

问题描述

我从 api ( ) 获得了一个包含用户详细信息的 json readUserFromApi。该 api 还提供了其响应的类型 ( interface User),并且当/如果 api 更新其响应结构时,此接口会自动导入到我的代码库中。

我从 api 读取 json 并添加其他有用的属性,创建detailedUser. 创建时detailedUser我只想提供api 未提供属性。将来,如果api 也将为name用户提供,我interface User的代码中的namedetailedUsername

我有一个解决方案,请参阅dummyVar,但应该有一个更优雅的解决方案......

代码

type Common<A, B> = {
  [P in keyof A & keyof B]: A[P] | B[P];
}

type Exactly<T, U extends T> = T & Record<Exclude<keyof U, keyof T>, never>

interface User {
    id: number
}

interface DetailedUser {
    id: number
    name: string
    sex: string
}

const readUserFromApi = () => ({id: 1})
const user: User = readUserFromApi()

const detailedUser: DetailedUser = {
    // I want an error if by mistake I provide a default value for `id`,
    // I dont have to do that, `id` comes from `user`
    name: 'default name',
    sex: 'f',
    ...user
}

// when interface User changes in the future and get a new property `name`,
// line below will trigger a typescript error - as I expect
declare const dummyVar: Exactly<Common<typeof user, {name: any, sex: any}>, {}>

标签: typescript

解决方案


对于DetailedUser类型,如果我们使用类型别名而不是接口,我们可以直接在类型中得到错误。对于像这种对象类型这样的简单类型,应该没有实际的区别。

interface User {
    id: number
    //name: string // comment this and you get an error below
}
type SafeExtend<TBase, TExt extends Record<Extract<keyof TBase, keyof TExt>, never>> = TBase & TExt
type DetailedUser = SafeExtend<User, {
    name: string
    sex: string
}>

SafeExtend将采用基本接口 ( User) 和包含您要添加的任何额外字段的类型。如果添加的字段已经在基本界面中,您将收到错误消息。

对于第二部分,如果您指定字段,您希望在传播中出现错误,我认为使用传播是不可能的。如果您改用函数,则可以这样做:

function safeSpread<TBase, TExt>(base: TBase, ext: TExt & Record<Extract<keyof TBase, keyof TExt>, never> ) {
    return {
        ...base, 
        ...ext
    };
}

const detailedUser: DetailedUser = safeSpread(user, {
    name: 'default name',
    sex: 'f',
    id: 0 // error
})

推荐阅读