打字稿如何键入字符串文字数组的子集

时间:2020-06-05 12:51:57

标签: typescript typescript-generics

我有一个功能, createFields ,它带有一个通用对象类型,在这种情况下是User,带有一组键。我希望这些键的子集类型可以通过字符串文字数组来推断,例如Fields中的 selectable 属性。我可以用打字稿吗?如果是,我如何实现?谢谢。

Here is the code
interface User {
  id: number;
  username: string;
  password: string;
  age: number;
}

interface Fields<T extends object = {}> {
  selectable: Array<keyof T>;
  sortable: Array<keyof T>;
}

function createFields<T extends object = {}>({ selectable, sortable }: Fields<T>) {
  return {
    // How can I get type of selectable to be ("id" | "username" | "age")[]
    select(fields: typeof selectable): string {
      return fields.join(',')
    }
  }
}
const UserFields: Fields<User> = {
  selectable: ['id', 'username', 'age'],
  sortable: ['username', 'age']
}

const fields = createFields(UserFields)
// actual
// => fields.select = (fields: ("id" | "username" | "password" | "age")[]): string;

// expected 
// => => fields.select = (fields: ("id" | "username" | "age")[]): string;

1 个答案:

答案 0 :(得分:1)

问题在于Fields<T>不够通用。它的selectablesortable属性是Array<keyof T>,太宽了以至于无法记住使用了keyof T的哪个子集。我要处理的方法是使createFields()接受从constrainedF的通用类型Fields<T>

执行此操作时面临的一个问题是编译器无法执行partial type argument inference。您需要同时指定TF,或者让编译器同时推断两者。编译器确实无法推断出T。假设您要指定T的方式是使用currying:

const createFieldsFor =
    <T extends object>() => // restrict to particular object type if we want
        <F extends Fields<T>>({ selectable, sortable }: F) => ({
            select(fields: readonly F["selectable"][number][]): string {
                return fields.join(',')
            }
        })

如果您不在乎特定的T,则可以完全不指定它:

const createFields = <F extends Fields<any>>({ selectable, sortable }: F) => ({
    select(fields: readonly F["selectable"][number][]): string {
        return fields.join(',')
    }
})

请注意,fields的参数为readonly F["selectable"][number][],表示:查找"selectable"的{​​{1}}属性,并查找其F索引类型( number)...,我们接受该类型的任何数组或只读数组。我不仅仅使用F["selectable"][number]的原因是,如果该类型是具有固定顺序和长度的元组,我们不希望F["selectable"]要求相同的顺序/长度。


对于咖喱或非咖喱情况,您需要将fields设置为不会扩展为UserFields且不会扩展Fields<T>和{{1}的对象类型}至selectable。这是一种实现方法:

sortable

我使用const assertion来缩小string[]的范围。这是使const UserFields = { selectable: ['id', 'username', 'age'], sortable: ['username', 'age'] } as const; // const assertion doesn't forget about sting literals 不接受的数组UserFields的副作用,因此我们可以更改readonly以允许常规和只读数组:

Fields<T>

或者您也可以用其他方法制作Fields<T>。关键是您不能将其注释为interface Fields<T extends object> { // readonly arrays are more general than arrays, not more specific selectable: ReadonlyArray<keyof T>; sortable: ReadonlyArray<keyof T>; } ,否则它将忘记您关心的所有内容。


咖喱溶液的用法如下:

UserFields

请注意Fields<User>中如何发生错误,因为const createUserFields = createFieldsFor<User>(); const fields = createUserFields(UserFields); // okay const badFields = createUserFields({ selectable: ["password"], sortable: ["id", "oops"] }); // error! // "oops" is not assignable to keyof User ------------------> ~~~~~~ 会抱怨非badFields属性。

让我们确保出现的createUserFields()可以按预期工作:

keyof User

这就是您想要的,对吧?


非咖喱的不在乎fields解决方案与之类似,但是它无法捕获fields.select(["age", "username"]); // okay fields.select(["age", "password", "username"]); // error! // -----------------> ~~~~~~~~~~ // Type '"password"' is not assignable to type '"id" | "username" | "age"'

User

我想您要使用哪个(如果有)取决于您的用例。这里的重点是您需要"oops"const alsoFields = createFields(UserFields); const alsoBadFields = createFields({ selectable: ["password"], sortable: ["id", "oops"] }); // no error createFields()属性中的键类型上是通用的。确切的操作方式取决于您。希望能有所帮助;祝你好运!

Playground link to code