如何创建此映射类型以将输入减少到所需的形状?

How can I create this mapped type to reduce the input to the desired shape?

我有以下 State 形状,想定义一个扁平化的 slice 常量,它具有状态属性的类型,而不需要再次显式 define/reference 它们,所以我会像 MapFlatSlice:

这样的映射类型
type State = {
  fruit: {
    mango: 'haden' | 'keitt';
    papaya: 'sunrise' | 'strawberry';
  };
  colors: {
    green: 10 | 20 | 30;
  };
  season: {
    nartana: 'april' | 'may' | 'june' | 'july' | 'august';
  };
};

type MapFlatSlice = {
  // ...
};

const slice = {
  fruit: ['mango', 'papaya'],
  colors: ['green'],
  season: ['nartana'],
} as const;

type S = MapFlatSlice<typeof slice>;

其中 S 应该是:

type S = {
  mango: 'haden' | 'keitt';
  papaya: 'sunrise' | 'strawberry';
  green: 10 | 20 | 30;
  nartana: 'april' | 'may' | 'june' | 'july' | 'august';
};

在上面的例子中,slice 用于类似于 redux 的 mapStateToProps,像这样:

const makeSlice = (s: Slice): (t: MapFlatSlice<typeof s>) => void => {
  // ...
};

所以给出:

const s1 = makeSlice(slice);

那么 s1 将是:

const s1 = (props: {
  mango: "haden" | "keitt";
  papaya: "sunrise" | "strawberry";
  green: 10 | 20 | 30;
  nartana: "april" | "may" | "june" | "july" | "august";
}) => {};

(其中s1props的类型与上面的S相同)


所以我认为 MapFlatSlice 应该类似于...

type MapFlatSlice<S extends { [k in keyof State]?: readonly (keyof State[k])[] }> = {
  [k in keyof S]: {
    [_k in S[k][number]]: ...
  };
};

但我不知道如何“展平”它,而且用 number 索引 S[k] 也不起作用。

使用实用程序类型 ArrayType 从您的 const slice 数组中提取键:

type ArrayType<T extends readonly any[] | undefined> = 
  T extends readonly (infer V)[] ? V : never; 

您可以使用以下方法将所有嵌套对象提取到联合中:

// Extract a union of the nested object properties
// State -> { mango: ...; papaya: ...; } | { green: ...; } | { nartana: ...; }
type GetNestedProps<State, Slice extends {[K in keyof State]?: readonly (keyof State[K])[]}> = {
    [K in keyof Slice]: K extends keyof State 
                     // ^ This typeguard lets us use `K` to index `State` in `Pick`
                     // where we'll select only the keys passed in by the `slice` object
        ? Pick<State[K], (ArrayType<Slice[K]> extends keyof State[K] ? ArrayType<Slice[K]> : never)> 
        //                                    ^ this is a necessary typeguard to make sure we can use the
        //                                      return of ArrayType<Slice[K]> as an index on State[K]
        : never
}[keyof Slice];
// ^ indexing with the keyof, we get a union of the values on the object

任何时候您想要使用不是从对象中提取的键进行索引,您都需要使用类型保护来确保编译器可以将该键用作对象的索引。如果您注意到,我在类型参数中将键设为可选。这将允许您排除 const slice 中的键而不是空数组。如果你喜欢强制空数组,你可以从 [K in keyof State]?.

中删除问号

现在您有了嵌套属性的并集,您可以使用实用程序类型 UnionToIntersection 将其转换为您想要的类型:

// X | Y -> X & Y
type UnionToIntersection<T> = 
  (T extends any ? (x: T) => any : never) extends 
  (x: infer R) => any ? R : never;

最后,你的实际类型,我在其中添加 State 作为类型参数以使其可跨状态重用:

type MapFlatSlice<State,Slice extends {[K in keyof State]?: readonly (keyof State[K])[]}> = 
  UnionToIntersection<GetNestedProps<State,Slice>>;

可用作:

const slice = {
  fruit: ['mango','papaya'],
  color: ['green'],
  season: ['nartana'],
} as const;

type S = MapFlatSlice<State,typeof slice>;

const s: S = {
    mango: 'haden',
    papaya: 'sunrise',
    green: 10,
    nartana: 'april'
}

playground link

我倾向于按如下方式处理:


哪些类型是“可切片的”?也就是说,当你写MapFlatSlice<T>时,T允许的类型是什么?我认为它是 T extends Sliceable<T> 的任何类型,其中 Sliceable<T> 定义为:

type Sliceable<T> =
  { [K in keyof T]: K extends keyof State ? ReadonlyArray<keyof State[K]> : never }

也就是说,如果 T 是可切片的,则键 K 处的每个 属性 必须是 State[K] 键的(可能是只读的)数组。如果 K 不是 State 的键,那么在键 K 处应该没有 属性 (所以 属性 的类型是 never ).


现在我们已经对 MapFlatSlice 的输入进行了限制,输出应该是什么?我发现这最容易分成两步。我们将采用输入类型 T 并“反转”键和值,以便为我们提供更接近我们想要的形状的东西,其中仍然有足够的信息来完成工作。

这里是:

type InvertKeyValues<T extends Record<keyof T, readonly PropertyKey[]>> =
  { [K in keyof T as T[K][number]]: K };

如果我们将其直接应用于 typeof slice,您将看到我们得到的结果:

type Intermediate = InvertKeyValues<typeof slice>;    
/* type Intermediate = {
    readonly mango: "fruit";
    readonly papaya: "fruit";
    readonly green: "color";
    readonly nartana: "season";
} */

键是你想要在最终输出中的键,值是我们需要参考的State中的相关键。


给定中间类型作为新输入 U,我们可以进行最终转换:

type DoSlice<U> =
  { -readonly [K in keyof U]: Idx<Idx<State, U[K]>, K> }
    

回想一下,U[K]State 的键,而 K 是子键。所以我们要写State[U[K]][K]。但是编译器无法判断这样的索引访问是否有效,所以我们使用 Idx<O, P> 代替我们现在定义的 O[P]


假设我们有一个对象类型 O 和一个键类型 P,但编译器甚至不知道 PO 的键尽管我们认为是。这意味着我们不能直接写 indexed access type O[P] 而不会出错。相反,我们可以定义一个 Idx 类型别名,它在索引之前检查 P

type Idx<O, P> = P extends keyof O ? O[P] : never;

现在,任何地方 O[P] 都会产生错误,我们可以用 Idx<O, P> 替换它。因此在 DoSlice 中,State[U[K]][K] 变为 Idx<Idx<State, U[K]>, K>.


最后,MapFlatSlice<T>根据约束和输出操作定义:

type MapFlatSlice<T extends Sliceable<T>> =
  DoSlice<InvertKeyValues<T>>

那么,它有效吗?

type S = MapFlatSlice<typeof slice>
/* type S = {
    mango: "haden" | "keitt";
    papaya: "sunrise" | "strawberry";
    green: 10 | 20 | 30;
    nartana: "april" | "may" | "june" | "july" | "august";
} */

看起来不错。您可以验证其他映射是否也应该有效,以及向 slice 添加不正确的属性是否会导致编译器警告。

Playground link to code