如何创建此映射类型以将输入减少到所需的形状?
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";
}) => {};
(其中s1
中props
的类型与上面的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'
}
我倾向于按如下方式处理:
哪些类型是“可切片的”?也就是说,当你写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
,但编译器甚至不知道 P
是 O
的键尽管我们认为是。这意味着我们不能直接写 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
添加不正确的属性是否会导致编译器警告。
我有以下 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";
}) => {};
(其中s1
中props
的类型与上面的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'
}
我倾向于按如下方式处理:
哪些类型是“可切片的”?也就是说,当你写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
,但编译器甚至不知道 P
是 O
的键尽管我们认为是。这意味着我们不能直接写 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
添加不正确的属性是否会导致编译器警告。