打字稿-确保通用属性存在描述错误的通用类型

时间:2019-01-02 22:24:01

标签: typescript generics

(打字稿新手警告)

我正在创建一个可重用的化简器,它接受一个状态和一个动作并返回该状态,但仅限于接受在给定键处包含某种类型的状态。该键作为函数的参数传递。如果传递的状态对象不包含传递的密钥,则编译器应引发错误。

现在,我已经开始工作了。 但是,据我估计,编译器生成的错误消息不足以描述问题。它并没有说“ x属性在类型上丢失”,而是给出了其他错误,我将在下面详细介绍。

类型

// FetchAction up here, not so relevant...

export type FetchState = {
  status: FetchAction,
  timestamp: Date
} | null

export type LoginState = {
  token: string | null,
  fetching: FetchState
};

Base Reducer

const intialState: LoginState = {
  token: null,
  fetching: null
}

const loginReducer: Reducer<LoginState> = (state = intialState, action) => {
  //...other operations 
  return fetchingReducer(state, action, 'fetching');
}

方法1

type FetchContainingState<S, K extends keyof S> = {
  [F in keyof S]: F extends K ? FetchState : S[F];
};

export const fetchingReducer = <S extends FetchContainingState<S, K>, K extends keyof S>(state: S, action: Action, key: K): S => {
  // implementation
}

这正常工作。如果我将函数调用更改为: return fetchingReducer(state, action, 'fetchin');(拼写错误fetching),然后出现此错误:

  

'LoginState'类型的参数不能分配给类型的参数   'FetchContainingState'。种类   属性“令牌”不兼容。       输入'string | “ null”不可分配给“ FetchState”类型。         不能将“字符串”类型分配给“ FetchState”类型。

好吧,这给了我一个错误。但是,它只是警告我"token"或对象上存在的任何其他属性。它并不能直接指示我期望使用哪个属性,但是不存在。

方法2

type EnsureFetchState<S, K extends keyof S> = S[K] extends FetchState ? S : never;

export const fetchingReducer = <S, K extends keyof S>(state: EnsureFetchState<S, K>, action: Action, key: K): S => {
   // implementation
}

当我将呼叫更改为return fetchingReducer(state, action, 'fetchin');(拼写错误"fetching")时,也可以得到:

  

'LoginState'类型的参数不能分配给'never'类型的参数。

更简洁,但对错误的描述更少。对于传递的参数可能出什么问题,它给出的指示甚至更少。

结论

在方法1中,我使用了映射类型,在方法2中,我使用了条件类型来确定我传递的statekey的值未满足我们正在搜索的条件。但是,在这两种情况下,错误消息都没有真正描述什么是真正的问题。

我是Typescript中具有更高级类型的新手,所以可能有一种非常简单的方法来执行此操作,或者是一个我忽略的简单概念。但愿如此!但是无论如何,要旨是:如何用惯用方式对具有动态键的对象进行这种类型的检查,或者以一种使编译器生成更有益的错误消息的方式进行检查?

2 个答案:

答案 0 :(得分:2)

在描述类型约束时,通常有助于尽可能直接。类型为S且属性为K且类型为FetchState的类型export const fetchingReducer = <S extends {[k in K]: FetchState}, K extends string>(state: S, action: Action, key: K): S => { 的约束不需要提及其他属性:

export interface Action {
    a: string;
}
export interface FetchAction extends Action {
    f: string;
}
export type FetchState = {
  status: FetchAction,
  timestamp: Date
} | null
export type LoginState = {
  token: string | null,
  fetching: FetchState
};
const intialState: LoginState = {
  token: null,
  fetching: null
}
export type Reducer<S> = (s: S, a: Action) => S;

const loginReducer: Reducer<LoginState> = (state = intialState, action) => {

  fetchingReducer(state, action, 'fetcing'); // Argument of type 'LoginState' 
        // is not assignable to parameter of type '{ fetcing: FetchState; }'.
       //  Property 'fetcing' is missing in type 'LoginState'.


  fetchingReducer(state, action, 'token'); // Argument of type 'LoginState' is 
          // not assignable to parameter of type '{ token: FetchState; }'.
         //  Types of property 'token' are incompatible.
        //    Type 'string | null' is not assignable to type 'FetchState'.
       //       Type 'string' is not assignable to type 'FetchState'.


  // OK
  return fetchingReducer(state, action, 'fetching')
}

export const fetchingReducer = <S extends {[k in K]: FetchState}, K extends string>(state: S, action: Action, key: K): S => {
  return {} as S;
}

这似乎会产生所需的错误消息,至少使用以下示例代码(我只是为缺少的类型组成了定义以使其完整):

[output1, output2] = some_func()

答案 1 :(得分:2)

错误的可读性,就像情人眼中的情人一样。

我认为,我能得到的最漂亮的错误是添加第二个重载,其中KPropertyKey。该错误是由条件类型触发的,但条件类型已添加到key参数上。第二次重载的需要来自这样一个事实:如果K extends keyof S并且在key上有错误,K会被推断为keyof S而不是实际值。

对于错误部分,我使用带有描述性消息的字符串文字类型。如果您有一个名为"This porperty is not of type FetchState"的密钥,则可能与此有关,但这似乎不太可能。

type EnsureFetchState<S, K extends PropertyKey> = S extends Record<K, FetchState> ? {} : "This porperty is not of type FetchState";
export function fetchingReducer<S, K extends keyof S>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S
export function fetchingReducer <S, K extends PropertyKey>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S
export function fetchingReducer <S, K extends keyof S>(state: S, action: Action, key: K & EnsureFetchState<S, K>): S {
   // implementation
}

//Argument of type '"fetchin"' is not assignable to parameter of type '"fetchin" & "This porperty is not of type FetchState"'. 
return fetchingReducer(state, action, 'fetchin');