typescript - 如何拥有一个显式类型参数和一个推断类型参数?
问题描述
我正在使用一个 UI 库,该库具有用于构建类似于此的表的 API:
type Column<Record> = {
keys: string | Array<string>;
render: (prop: any, record: Record) => React.ReactNode;
}
render 函数的第一个参数将由库提供,方法是 basic column.render(record[column.keys], record)
。如果column.keys
是一个数组,它会被解释为记录下的“路径”,大约是这样的record[keys[0]][keys[1]]...[keys[keys.length - 1]]
:下面的示例略有改动,因此它使用Pick<...>
算法代替,只是为了有一个更简单但仍然有效的示例。
// Our record type
interface Entity {
a: string;
b: number;
c: boolean;
}
// helper type, does the following:
// GetOrPickProps<Entity, 'a'> -> Entity['a']
// GetOrPickProps<Entity, ['b', 'c']> -> Pick<Entity, 'a' | 'c'>
type GetOrPickProps<E, K extends (keyof E | Array<keyof E>)> = K extends keyof E
? E[K]
: K extends Array<infer K2>
? Pick<E, K2 & keyof E>
: never;
// My first attempt at a Column type
type Column<E, K extends (keyof E | Array<keyof E>)> = {
keys: K;
render: (prop: GetOrPickProps<E, K>) => string;
}
// ...but it doesn't work
const columns: Array<Column<Entity, /* What goes here??? */>> = [
{
keys: 'a',
render: a => a,
},
{
keys: ['a', 'c'],
render: ({ a, c }) => c ? a : 'something else',
}
]
如果我明确地将'a' | ['a', 'c']
第二个参数设置为Column
,则两个渲染函数的类型都是(prop: Entity['a'] | Pick<Entity, 'a' | 'c'>) => string
.
如果我将第二个参数设为Column
可选(可能像K extends ... = unknown
) typescript 实际上不会再推断类型,而是显式unknown
使用prop
.
我怎样才能有一个类型来推断它的部分道具以约束其他道具,但也接受显式类型参数?
TS-游乐场在这里。
解决方案
请让我知道它是否适合您:
interface Entity {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: (arg: Arg) => string
}
type Keys = 'a' | ['a', 'c'];
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Record<H, H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Record<H, H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<string>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Result = Mapped<UnionToArray<'a' | ['a', 'c']>>;
const columns: Result = [
{
keys: 'a',
render: a => a,
},
{
keys: ['a', 'c'],
render: ({ a, c }) => c ? a : 'something else',
}
]
如果您同意此解决方案,我将提供更多解释。
除此之外,here和here,在我的博客中,您可以找到一些解释
这是Reducer
实用程序类型的 js 类似物
const reducer = (arr: ReadonlyArray<Elem>, result: Record<string, any> = {}): Record<string, any> => {
if (arr.length === 0) {
return result
}
const [head, ...tail] = arr;
return reducer(tail, { ...result, [head]: 'string' })
}
更新
您可以使用辅助功能,但有一个缺点。您仍然必须提供显式类型。好消息 - 如果不允许显式类型,TS 会抱怨。
interface Data {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: <T extends Arg>(arg: T) => string
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Predicate<H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Predicate<H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<keyof Data>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Format<Keys extends ReadonlyArray<keyof Data>> =
UnionToArray<Keys[number]> extends ReadonlyArray<string>
? Reducer<UnionToArray<Keys[number]>>
: never
interface Union<Keys extends keyof Data> {
keys: Keys,
render: (a: Data[Keys]) => void
}
interface WithArray<Keys extends ReadonlyArray<keyof Data>> {
keys: Keys,
render: (a: Format<Keys>) => void
}
type Base<Keys> = Keys extends Array<keyof Data> ? WithArray<Keys> : Keys extends keyof Data ? Union<Keys> : any
const builder = <V extends Array<keyof Data>, U extends keyof Data>(columns: [...(Base<[...V]> | Base<U>)[]]) => columns
const result = builder([{
keys: ['c'], render: (prop: Record<"c", boolean>) => prop
}, {
keys: 'b', render: (prop) => prop
}])
您还可以合并所有可能的值。就像我在这里做的一样。但是如果你的Entity
界面包含超过 5 个道具,它就不起作用了。
或者,您可以为每个实体使用 helper:
interface Data {
a: string;
b: number;
c: boolean;
}
type Column<Key, Arg> = {
keys: Key,
render: <T extends Arg>(arg: T) => string
}
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type UnionToOvlds<U> = UnionToIntersection<
U extends any ? (f: U) => void : never
>;
// credits goes to https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
// credits https://stackoverflow.com/users/125734/titian-cernicova-dragomir
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
: [T, ...A];
type ReducerElem = string;
type Predicate<T> = T extends keyof Data ? Record<T, Data[T]> : never
type Reducer<
Arr extends ReadonlyArray<ReducerElem>,
Result extends Record<string, any> = {}
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends ReducerElem
? Result & Predicate<H>
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<ReducerElem>
? H extends ReducerElem
? Reducer<Tail, Result & Predicate<H>>
: never
: never
: never;
type MapPredicate<T> =
T extends string
? Column<T, T>
: T extends Array<keyof Data>
? Column<T, Reducer<T>>
: never
type Mapped<
Arr extends Array<unknown>,
Result extends Array<unknown> = []
> = Arr extends []
? []
: Arr extends [infer H]
? [...Result, MapPredicate<H>]
: Arr extends [infer Head, ...infer Tail]
? Mapped<[...Tail], [...Result, MapPredicate<Head>]>
: Readonly<Result>;
type Format<Keys extends ReadonlyArray<keyof Data>> =
UnionToArray<Keys[number]> extends ReadonlyArray<string>
? Reducer<UnionToArray<Keys[number]>>
: never
function maker<K extends Array<keyof Data>>(keys: K, cb: (a: Format<K>) => void): { keys: K, render: (a: Format<K>) => void }
function maker<K extends keyof Data>(keys: K, cb: (a: Data[K]) => void): { keys: K, render: (a: Data[K]) => void }
function maker(keys: keyof Data | Array<keyof Data>, cb: (...args: any[]) => void) {
return {
keys,
render: cb
}
}
const COLUMNS = [
maker('a', (prop /** stirng */) => { }),
maker(['a'], (prop /** Record<"a", string> */) => { }),
maker('b', (prop /** Record<"a", string> */) => { })
] as const
推荐阅读
- mac-address - 如何使用 MAC 地址阻止 OpenVPN 访问
- python - 使用 BeautifulSoup在流文本B中刮取 B
- python - 通过重复序列填充列
- excel - Power Pivot 多对多关系
- google-apps-script - 使用 Google 应用脚本更改 Google 表格共享设置
- typescript - 如何使用 Typescript 在 Material UI 中扩展调色板
- swift - 有没有办法让 SwiftUI 文本标签在显示计时器时不会轻微移动?
- javascript - FindDomNode 弃用警告,尽管使用了 ref
- java - 如何在不损坏的情况下将图像从 java 套接字服务器发送到导航器?
- laravel - 无法打开 laravel.log - ec2 Aws 中的权限错误