如何拥有一个显式类型参数和一个推断类型参数? [英] How can I have one explicit and one inferred type parameter?

查看:22
本文介绍了如何拥有一个显式类型参数和一个推断类型参数?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用一个 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆