如何设置返回类型为 V extends keyof infer U

huangapple go评论63阅读模式
英文:

How can I set the return type to be V extends keyof infer U

问题

Here is the translated code without any additional information:

接口中有基本类型的键和对象数组的键:

interface Ifields {
    name: string,
    items: {
        id: number
    }[]
}

我正在为一个将具有以下功能的对象创建一个新接口:

const sublistvalue = rec.getSublistValue({sublistId: 'items', fieldId: 'id', line: 0})

我想创建另一个接口,该接口获取 Ifields 接口并只允许我传递接口中是数组的键以及在该数组中的字段。

const rec = makeRecord() as IRecord<IFields>
rec.getSublistValue({sublistId: 'name', fieldId: 'id', line: 0}) //invalid name is not an array
rec.getSublistValue({sublistId: 'items', fieldId: 'qty', line: 0}) // invalid 'qty' not a field of items

rec.getSublistValue({sublistId: 'items', fieldId: 'id', line: 0})// valid

此外,我希望该函数的返回类型为 number

我已经接近成功:

interface IFields {
    name: string,
    items: {
        id: number
    }[]
}

interface IRecord<T> {
    fn: <K extends keyof T>(x: {
        sublistId: T[K] extends object[] ? K : never,
        fieldId:  T[K] extends (infer U)[] ? keyof U : never,
        line: number}) => 
        T[K] extends (infer  U)[] ? U : never
}

这几乎可以工作,但最后一个要求未满足。

在这种情况下,返回类型 `U`  `{id: number}`,我需要它是 `number`

以下是带有我的当前解决方案的 TypeScript Playground 链接:
[link](https://www.typescriptlang.org/play?ssl=23&amp;ssc=1&amp;pln=1&amp;pc=1#code/JYOwLgpgTgZghgYwgAgJIEZkG8BQz-IhwC2EAXMgM5hSgDmANHgcJMZRbgd8sACYUQAV2IAjaM3wBfANoBdHFJzKcoSLEQpUAJgA8AFQB82SchggKugNLIIAD0gg+lZAGsIATwD2MZEYAUdpym3JRCogA2wNSoAn4yVnK2DhBOLl6iAFYQCGDyAPzIVoIQAG7QTDwEMMAQEXyxFPGJyY7OyP6gMNDIAKoAlAVunj59JeVQlVXIUSDkhCLiUFL9yCEEALzG+glJ9m0unSDdUPgD8siFvcjjEkrKCF4g1Mh2yBvYUshwLjq6GIYHk8XpAXh87AA6cz+LBhSLRMCNZAAIlYEHYyIYZlq9SRqL4mJmoHmAAYVsogA)
英文:

I have an interface that has keys that are primitive types and keys that are arrays of objects:

interface Ifields {
    name: string,
    items: {
        id: number
    }[]
}

I'm creating a new interface for an object that will have the following function:


const sublistvalue = rec.getSublistValue({sublistId: &#39;items&#39;, fieldId: &#39;id&#39;, line: 0})

I want to create another interface that gets the Ifields interface and only allows me to pass in keys in the interface that are arrays, and fields that are in that array.

const rec = makeRecord() as IRecord&lt;IFields&gt;
rec.getSublistValue({sublistId: &#39;name&#39;, fieldId: &#39;id&#39;, line: 0}) //invalid name is not an array
rec.getSublistValue({sublistId: &#39;items&#39;, fieldId: &#39;qty&#39;, line: 0}) // invalid &#39;qty&#39; not a field of items

rec.getSublistValue({sublistId: &#39;items&#39;, fieldId: &#39;id&#39;, line: 0})// valid

In addition I want the function to have a return type of number.

I'm pretty close:

interface IFields {
    name: string,
    items: {
        id: number
    }[]
}

interface IRecord&lt;T&gt; {
    fn: &lt;K extends keyof T&gt;(x: {
        sublistId: T[K] extends object[]? K: never,
        fieldId:  T[K] extends (infer U)[]? keyof U: never,
        line: number}) =&gt; 
        T[K] extends (infer  U)[] ? U : never
}

This almost works, but the last requirement is not satisfied.

The return type U in this case is {id: number}, I need it to be number

Here is a link in typescript playground with my current solution:
link

答案1

得分: 1

以下是您请求的翻译:

如果您想跟踪数组元素的特定 fieldId 属性,那么您还需要使 fn() 以该类型为泛型。 让我们将 K 保留为 sublistId 的类型,并为 fieldId 的类型参数命名为 P。 然后,您可以像这样编写 I2<T>

interface I2<T> {
    fn: <K extends ArrayKeys<T>, P extends keyof ArrayElement<T[K]>>(x: {
        sublistId: K,
        fieldId: P,
        line: number
    }) => ArrayElement<T[K]>[P]
}

其中 ArrayKeys<T>ArrayElement<T> 是如下定义的辅助实用类型:

type ArrayKeys<T> = keyof {
    [K in keyof T as T[K] extends readonly object[] ? K : never]:
    any }
    
type ArrayElement<T> =
    T extends readonly any[] ? T[number] : never;

这个想法是,K 应该只是满足约束的 T 属性的键。如果编译器能够推断泛型条件类型(例如 ArrayKeys<T>),那么您可能只需编写 P extends T[K][number],并且返回类型将是 T[K][number][P],因为它会理解 T[K] 是具有对应 number 索引签名 的数组类型,因此您可以使用 number 键对其进行索引访问来获取数组元素类型。但它无法执行这样的操作,因此我们必须使用实用程序类型 ArrayElement<> 来帮助它。所以,我们将 T[K][number] 替换为 ArrayElement<T[K]>


让我们来测试一下:

interface I1 {
    name: string,
    items: {
        id: number,
        str: string
    }[],
    other: {
        foo: boolean
    }[]
}
const x = {} as I2<I1>
x.fn({ sublistId: "items", fieldId: "id", line: 0 }) // number
x.fn({ sublistId: "items", fieldId: "str", line: 1 }) // string
x.fn({ sublistId: "items", fieldId: "wha", line: 1 }) // error
// ----------------------> ~~~~~~~ 
// Type '"wha"' is not assignable to type '"id" | "str"'
x.fn({ sublistId: "name", fieldId: "str", line: 1 }) // error
// --> ~~~~~~~~~          ~~~~~~~
// Type '"name"' is not assignable to type '"items" | "other"'
x.fn({ sublistId: "other", fieldId: "foo", line: 2 }) // boolean

看起来不错。编译器接受有效的输入,拒绝无效的输入(不对应数组的 sublistId 或不对应正确键的 fieldId),并返回预期的类型。


请注意,如果您不想编写实用程序类型,您可以内联定义,如下所示:

interface I2<T> {
    fn: <K extends keyof {
        [K in keyof T as T[K] extends readonly object[] ? K : never]: any
    }, P extends keyof (
        T[K] extends readonly any[] ? T[K][number] : never
    ) >(x: {
        sublistId: K,
        fieldId: P,
        line: number
    }) => (T[K] extends readonly any[] ? T[K][number] : never)[P]
}

它基本上可以工作一样,尽管更混乱,因此取决于您的用例是否合适。

英文:

If you want to keep track of the particular fieldId property of the array elements, then you will need fn() to be generic in that type as well. Let's keep K as the type of sublistId, and give the type parameter name P to the type of fieldId. Then you can write I2&lt;T&gt; like this:

interface I2&lt;T&gt; {
fn: &lt;K extends ArrayKeys&lt;T&gt;, P extends keyof ArrayElement&lt;T[K]&gt;&gt;(x: {
sublistId: K,
fieldId: P,
line: number
}) =&gt; ArrayElement&lt;T[K]&gt;[P]
}

where ArrayKeys&lt;T&gt; and ArrayElement&lt;T&gt; are helper utility types defined like:

type ArrayKeys&lt;T&gt; = keyof {
[K in keyof T as T[K] extends readonly object[] ? K : never]:
any }
type ArrayElement&lt;T&gt; =
T extends readonly any[] ? T[number] : never;

The idea is that K should only be keys of T whose properties are arrays of objects (and note that readonly object[] is more permissive than object[] so we use that). So ArrayKeys&lt;T&gt; uses key remapping to include only keys whose properties meet the constraint.

If the compiler were able to reason about generic conditional types like ArrayKeys&lt;T&gt; then you could probably just write P extends T[K][number] and have the return type be T[K][number][P], since it would understand that T[K] is an array type with a corresponding number index signature and therefore you could index into it with a number key to get the array element type. But it can't do that. So we have to help it out by using a utility type ArrayElement&lt;&gt;. So instead of T[K][number] we have ArrayElement&lt;T[K]&gt;.


Let's test it out:

interface I1 {
name: string,
items: {
id: number,
str: string
}[],
other: {
foo: boolean
}[]
}
const x = {} as I2&lt;I1&gt;
x.fn({ sublistId: &quot;items&quot;, fieldId: &quot;id&quot;, line: 0 }) // number
x.fn({ sublistId: &quot;items&quot;, fieldId: &quot;str&quot;, line: 1 }) // string
x.fn({ sublistId: &quot;items&quot;, fieldId: &quot;wha&quot;, line: 1 }) // error
// ----------------------&gt; ~~~~~~~ 
// Type &#39;&quot;wha&quot;&#39; is not assignable to type &#39;&quot;id&quot; | &quot;str&quot;&#39;
x.fn({ sublistId: &quot;name&quot;, fieldId: &quot;str&quot;, line: 1 }) // error
// --&gt; ~~~~~~~~~          ~~~~~~~
// Type &#39;&quot;name&quot;&#39; is not assignable to type &#39;&quot;items&quot; | &quot;other&quot;&#39;
x.fn({ sublistId: &quot;other&quot;, fieldId: &quot;foo&quot;, line: 2 }) // boolean

Looks good. The compiler accepts valid inputs, rejects invalid inputs (sublistIds that don't correspond to arrays, or fieldIds that don't correspond to the right keys), and returns the expected type.


Note that if you don't want to write utility types you could inline the definitions like

interface I2<T> {
fn: <K extends keyof {
[K in keyof T as T[K] extends readonly object[] ? K : never]: any
}, P extends keyof (
T[K] extends readonly any[] ? T[K][number] : never
) >(x: {
sublistId: K,
fieldId: P,
line: number
}) => (T[K] extends readonly any[] ? T[K][number] : never)[P]
}

which works more or less the same, although it's messier and therefore depends on your use case whether or not it's appropriate.

Playground link to code

huangapple
  • 本文由 发表于 2023年4月20日 03:39:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76058241.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定