给定两个接口

// the "normal" state
interface State {
    index: number;
    offset: number;
}

// let's extend it with some additional fields
interface SpecialState extends State {
    search: string;
    filter: string;
}

我正在try 创建一个函数,该函数将接受SpecialState类型的分部对象作为参数,其中must包括扩展属性,还可以包括基类型的任何属性.因此,输入类型应该是这样的

type InitialState<T extends State> = Partial<State> & Omit<T, keyof State>;

这很好用,我可以创建一个对象,其中searchfilter是必需的,limitoffset是可选的.

const initialSpecialState: InitialState<SpecialState> = {
    search: 'hello',
    filter: 'bonjour',
    index: 2
};

该函数的结果将是扩展类型的完整对象(在本例中为SpecialState).也就是说,该函数将"知道"基本接口,并能够为这些属性生成默认值,以防它们在输入中丢失.但它不能为扩展属性生成任何缺省值,因为它们是未知的.因此,它们需要在输入中给出.

因此,该函数将是泛型的,并将基类型State的任何扩展作为类型参数.

function makeFinalState<T extends State>(initialState: InitialState<T>): T {
    const baseState: State = {
        index: 0,
        offset: 0,
    }

    // TS doesn't like this
    const mergedState: T = {
        ...baseState,
        ...initialState
    }

    return mergedState
};

这就是TypeScrip向我抛出错误的地方

'{ index: number; offset: number; } & Partial<State> & Omit<T, keyof State>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'State'

但是,该函数在我的用例中工作得很好,而不考虑错误

const finalState = makeFinalState<SpecialState>(initialSpecialState);
console.log(finalState)
// logs 
// {
//   "index": 2,
//   "offset": 0,
//   "search": "hello",
//   "filter": "bonjour"
// }

那么这里出了什么问题呢?或者我应该以一种完全不同的方式来做这件事?

Playground

推荐答案

打字文本类型判断器还不够复杂,无法抽象地推理出大约genericconditional types,就像makeFinalState()正文中的InitialState<T>一样.(Omit utility type是用条件类型实现的,T是泛型.)如果它能识别出Pick<T, keyof State> & Omit<T, keyof State>等同于T,那就太好了,但目前这超出了它的能力范围.有关这方面的更多信息,请参见microsoft/TypeScript#28884Pick<Foo, Bar> & Omit<Foo, Bar> !== Foo in Typescript?.

因此,如果您重构您的类型,这样就不需要这样的理解,那么您会过得更好.这里有一种可能的方法:

function makeFinalState<T extends Partial<State>>(initialState: T) {
    const baseState: State = {
        index: 0, offset: 0,
    }
    const mergedState = { ...baseState, ...initialState };
    return mergedState
};

编译器推断分布的结果是分布类型的intersection,这意味着您得到类似于State & T的结果.这相当于您想要的:

const finalState = makeFinalState(initialSpecialState);
// const finalState: 
//  { index: number; offset: number; } & Partial<State> & Omit<SpecialState, keyof State>

当然,这种类型有些难看,并且包含不必要的Partial<State>,但它很有效.


根据您的偏好,您可以得到一个更简单的类型输出:我们可以创建一个"Identity"mapped type,它只是将一个类型映射到它自己,以提示编译器计算该类型,而不是将它留为一个交叉点:

function makeFinalState<T extends Partial<State>>(initialState: T) {
    const baseState: State = {
        index: 0, offset: 0,
    }
    type Id<T> = { [K in keyof T]: T[K] } & {}
    const mergedState: Id<State & T> =
        { ...baseState, ...initialState }
    return mergedState
};

这将产生以下输出类型:

const finalState = makeFinalState(initialSpecialState);
/* const finalState: {
    index: number;
    offset: number;
    search: string;
    filter: string;
} */

这样可能更好...同样,这取决于你的喜好.

Playground link to code

Typescript相关问答推荐

当使用`type B = A`时,B的类型显示为A.为什么当`type B = A时显示为`any `|用A代替?

类型{...}的TypeScript参数不能赋给类型为never的参数''

对深度对象键/路径进行适当的智能感知和类型判断,作为函数参数,而不会触发递归类型限制器

在TypeScrip中分配回调时,参数的分配方向与回调本身相反.为什么会这样呢?

如何使用一个字段输入对象,其中每个键只能是数组中对象的id属性,而数组是另一个字段?

如何在TypeScript中将一个元组映射(转换)到另一个元组?

ANGLE找不到辅助‘路由出口’的路由路径

如何消除在Next.js中使用Reaction上下文的延迟?

有没有可能产生输出T,它在视觉上省略了T中的一些键?

类型TTextKey不能用于索引类型 ;TOption

在排版修饰器中推断方法响应类型

布局组件中的Angular 17命名路由出口

棱角分明-什么是...接口类型<;T>;扩展函数{new(...args:any[]):t;}...卑鄙?

如何为带有参数的函数类型指定类型保护?

类型';只读<;部分<;字符串|记录<;字符串、字符串|未定义&>';上不存在TS 2339错误属性.属性&q;x&q;不存在字符串

如何在Vuetify 3中导入TypeScript类型?

断言同级为true或抛出错误的Typescript函数

包含多个不同类构造函数的接口的正确类型?

对象可能未定义(通用)

具有来自其他类型的修改键名称的 TypeScript 通用类型