首页 > 解决方案 > 打字稿如何键入字符串文字数组的子集

问题描述

我有函数createFields,它采用一个通用的 Object 类型,在本例中是 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;

标签: typescripttypescript-generics

解决方案


问题是它Fields<T>不够通用。它selectablesortable属性是Array<keyof T>,它太宽以至于无法记住使用了哪个子集keyof T。我处理这个问题的方法是createFields()接受一个泛型类型F,它被限制Fields<T>.

您这样做时面临的一个问题是编译器无法进行部分类型参数推断。您要么需要同时指定Tand F,要么让编译器同时推断两者。编译器真的没有任何东西可以推断T. 假设您想指定T我这样做的方式是使用柯里化:

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(',')
    }
})

请注意, 的参数fieldsreadonly F["selectable"][number][],意思是:查找 的"selectable"属性F,并查找它的number索引类型 ( F["selectable"][number])... 我们接受该类型的任何数组或只读数组。我不只是使用的原因F["selectable"]是因为如果该类型是固定顺序和长度的元组,我们不希望fields需要相同的顺序/长度。


对于 curried 或 non-curried 情况,您需要创建一个不扩大到也不扩大到UserFields的对象类型。这是一种方法:Fields<T>selectablesortablestring[]

const UserFields = {
    selectable: ['id', 'username', 'age'],
    sortable: ['username', 'age']
} as const; // const assertion doesn't forget about sting literals

我用一个const断言来保持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"'

这就是你想要的,对吧?


non-curried don't-care-about-User解决方案类似,但不会捕获"oops"

const alsoFields = createFields(UserFields);
const alsoBadFields =
    createFields({ selectable: ["password"], sortable: ["id", "oops"] }); // no error

我猜你想使用哪一个(如果有的话)取决于你的用例。这里的要点是,您需要createFields()selectableandsortable属性中的键类型具有通用性。究竟如何做到这一点取决于你。希望有帮助;祝你好运!

Playground 代码链接


推荐阅读