如何拥有一个显式类型参数和一个推断类型参数? [英] How can I have one explicit and one inferred type parameter?
问题描述
我正在使用一个 UI 库,该库具有用于构建与此类似的表的 API:
I am working with a UI-library that has an API for building tables similar to this:
type Column<Record> = {
keys: string | Array<string>;
render: (prop: any, record: Record) => React.ReactNode;
}
渲染函数的第一个参数将由库提供,基本上是column.render(record[column.keys], record)
.如果 column.keys
是一个数组,它会解释为路径";记录下来,大概是这样的:record[keys[0]][keys[1]]...[keys[keys.length - 1]]
.下面的示例略有改动,因此它使用 Pick<...>
算法代替,只是为了有一个更简单但仍然有效的示例.
The first argument to the render function will be provided by the library by doing basically column.render(record[column.keys], record)
. If column.keys
is an array, it instead interprets as a "path" down the record, about like this: record[keys[0]][keys[1]]...[keys[keys.length - 1]]
. The example below is slightly altered so it uses the Pick<...>
algorithm instead, just to have a simpler but still functional example.
// 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<;实体,'a' | 'c'>) =>字符串
.
If I explicitly put 'a' | ['a', 'c']
as the second parameter to Column
, the type of both render functions will be (prop: Entity['a'] | Pick<Entity, 'a' | 'c'>) => string
.
如果我将第二个参数设为 Column
可选(可能像 K extends ... = unknown
),打字稿实际上不会再推断类型,而是明确使用unknown
作为 prop
的类型.
If I make the second parameter to Column
optional (maybe like K extends ... = unknown
) typescript won't actually infer the type anymore, instead explicitly using unknown
as the type of prop
.
我怎样才能有一个类型来推断它的一部分 props 以约束其他 props,但也接受显式类型参数?
How can I have a type that infers part of it's props in order to constrain other props, but also accepts an explicit type parameter?
TS-游乐场这里.
推荐答案
请告诉我它是否适合您:
Please let me know if it works for you:
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',
}
]
如果您同意此解决方案,我将提供更多说明.
If you agree with this solution I will provide more explanation.
Apart from that, here and here , in my blog, you can find some exmplanation
这是 Reducer
实用程序类型的 js 模拟
This is js analogue of Reducer
utility type
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 会抱怨.
You can use helper function, but there is drawback. You still have to provide explicit type. Good news - TS will complain if explicit type is not allowed.
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 个 props,它将不起作用.
You can also make union of all possible values. Smth similar like I did here. But if your Entity
interface contains more than 5 props, it will not work.
或者,您可以为每个实体使用帮助程序:
Or, you can use helper for each entity:
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
这篇关于如何拥有一个显式类型参数和一个推断类型参数?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!