简体   繁体   English

是否可以键入 typescript 中的数组,以便将其项目限制为仅允许与另一个数组中的项目共享 ID?

[英]Can an array in typescript be typed so its items are restricted to only allow shared id's with items in another array?

I am building an object with an index signature: ColumnAndColumnSettings .我正在构建一个带有索引签名的 object: ColumnAndColumnSettings I'd like to restrict columnSettings to only allow objects with id's shared in columns .我想限制columnSettings只允许在columns中共享 id 的对象。

  type Column = {
  colId: string,
  width?: number,
  sort?: number
  something?: string
}

type ColumnSetting = Pick<Column, 'colId' | 'width' | 'sort'>;

const columnExample = { colId: '1', something: 'test'};
const columnSettingExample1 = { colId: '1'};
const columnSettingExample2 = { colId: '2'};

export const ColumnAndColumnSettings: {
  [key: string]: { columns: Column[]; columnSettings: ColumnSetting[]};
} = {
  people: { columns: [columnExample] , columnSettings: [columnSettingExample1] }, // ok
  boats: { columns:  [columnExample], columnSettings: [columnSettingExample1, columnSettingExample2] }, // ok but want this to error, 
  //columnSettingExample2 does not share id with any item in columns
}

Playground Link 游乐场链接

Before we get started: the only way the compiler will even be able to notice the colId property values aren't just of type string is if you give it some hint that it should be tracking their literal types .在我们开始之前:编译器甚至能够注意到colId属性值不仅仅是string类型的唯一方法是如果你给它一些提示它应该跟踪它们的文字类型 The easiest way to do that is via a const assertion when you create the objects:最简单的方法是在创建对象时通过const断言

const columnExample = { colId: '1', something: 'test' } as const;
/* const columnExample: {  readonly colId: "1";  readonly something: "test"; } */
const columnSettingExample1 = { colId: '1' } as const;
/* const columnSettingExample1: {  readonly colId: "1"; } */
const columnSettingExample2 = { colId: '2' } as const;
/* const columnSettingExample2: {  readonly colId: "2"; } */

Now the compiler knows that columnExample has a colId of "1" instead of string , for instance.例如,现在编译器知道columnExamplecolId"1"而不是string

Moving on:继续:


There is, unfortunately, no specific type in TypeScript that behaves the way you'd like ColumnAndColumnSettings to behave.不幸的是,TypeScript 中没有特定类型的行为符合您希望ColumnAndColumnSettings的行为方式。

Instead, you could write a generic type like ColumnAndColumnSettings<T> where T is a mapping from the keys of your object to the set of allowable column ids for that property:相反,您可以编写一个通用类型,如ColumnAndColumnSettings<T> ,其中T是从 object 的键到该属性的允许列 ID 集的映射:

type ColumnAndColumnSettings<T extends Record<keyof T, string>> = {
  [K in keyof T]: {
    columns: (Column & { colId: T[K] })[],
    columnSettings: (ColumnSetting & { colId: T[K] })[]
  }
}

Which results in something like this:结果是这样的:

type Example = ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }>;
/* type Example = {
    cars: {
        columns: (Column & {
            colId: "3" | "4";
        })[];
        columnSettings: (ColumnSetting & {
            colId: "3" | "4";
        })[];
    };
    trucks: {
        columns: (Column & {
            colId: "5";
        })[];
        columnSettings: (ColumnSetting & {
            colId: "5";
        })[];
    };
} */

But you don't want to have to write out the particular T argument.但是您不想写出特定的T参数。 It would be nice if the compiler could infer it for you.如果编译器可以为您推断出它,那就太好了。 And it can, but not directly via type annotation;它可以,但不能直接通过类型注释; you can't do this:你不能这样做:

const ColumnAndColumnSettings: ColumnAndColumnSettings = { // error!
// --------------------------> ~~~~~~~~~~~~~~~~~~~~~~~
  cars: { columns: [ce3, ce4], columnSettings: [ce3] },
  trucks: { columns: [ce5], columnSettings: [ce5] },
}; 

There's an open issue at microsoft/TypeScript#32794 asking for this, and maybe eventually you could write that or something like ColumnAndColumnSettings<infer> , but for now it's not part of the language.microsoft/TypeScript#32794有一个未解决的问题要求这个,也许最终你可以写那个或类似ColumnAndColumnSettings<infer>的东西,但现在它不是语言的一部分。


As I said though, this is possible if you refactor.正如我所说,如果你重构,这是可能的。 The general workaround for cases like this is to create a generic helper identity function whose sole purpose is to infer the type parameter.此类情况的一般解决方法是创建一个通用帮助程序标识 function,其唯一目的是推断类型参数。 That is, instead of const value: GenericType<infer> = { something: 123 } , you write a helper function of the form const asGenericType = <T,>(g: GenericType<T>) => g and then call it as const value = asGenericType({something: 123}) .也就是说,不是const value: GenericType<infer> = { something: 123 } ,而是编写一个const asGenericType = <T,>(g: GenericType<T>) => g形式的助手 function 然后将其称为const value = asGenericType({something: 123})

In this case we'd write在这种情况下,我们会写

const asColumnAndColumnSettings = <T extends Record<keyof T, string>>(
  c: ColumnAndColumnSettings<T>) => c;

And now we can use it:现在我们可以使用它了:

const c = asColumnAndColumnSettings({    
  cars: { columns: [ce3, ce4], columnSettings: [ce3] },
  trucks: { columns: [ce5], columnSettings: [ce5] },
});
/* const c: ColumnAndColumnSettings<{
  cars: "3" | "4";
  trucks: "5";
}> */

So T is inferred as desired.所以T是按需要推断的。 Great!伟大的!


Unfortunately, it's not quite right:不幸的是,这并不完全正确:

const ColumnAndColumnSettings = asColumnAndColumnSettings({
  people: { columns: [columnExample], columnSettings: [columnSettingExample1] }, // ok
  boats: { columns: [columnExample], 
    columnSettings: [columnSettingExample1, columnSettingExample2] }, // no error
});
/* const ColumnAndColumnSettings: ColumnAndColumnSettings<{
  people: "1";
  boats: "1" | "2";
 }> */

There's no error in the above, which was the whole point.上面没有错误,这就是重点。 The compiler was happy to infer T from both the columns property and the columnSettings property.编译器很乐意从columns属性columnSettings属性中推断出T You really only want to infer T from columns , and have the compiler check the columnSettings property against it.您真的只想从columns推断T ,并让编译器根据它检查columnSettings属性。

That is, the T in the definition of ColumnAndColumnSettings<T> inside the columnSettings property should be a non-inferential type parameter usage .也就是说, columnSettings属性里面的ColumnAndColumnSettings<T> T中的 T 应该是非推理类型参数用法 TypeScript doesn't directly support such a feature, and there is a issue in GitHub asking for it at microsoft/TypeScript#14829 . TypeScript 不直接支持这样的功能,GitHub 在microsoft/TypeScript#14829中有一个问题要求它。 Luckily though, there are workarounds mentioned in there.幸运的是,那里提到了解决方法。 The one I'll use here is to intersect the type parameter usage with the empty object type {} to lower its inference priority, as recommended :我将在此处使用的方法是将类型参数用法与空的 object 类型{} 相交,以降低其推理优先级, 如推荐的那样:

type ColumnAndColumnSettings<T extends Record<keyof T, string>> = {
  [K in keyof T]: {
    columns: (Column & { colId: T[K] })[],
    columnSettings: (ColumnSetting & { colId: T[K] & {} })[]
// ----------------------------------------------> ^^^^
  }
}

This acts much the same, and ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }>这与ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }> ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }> hasn't changed. ColumnAndColumnSettings<{ cars: "3" | "4", trucks: "5" }>没有改变。 But now:但现在:

const ColumnAndColumnSettings = asColumnAndColumnSettings({
  people: { columns: [columnExample], columnSettings: [columnSettingExample1] }, // ok
  boats: { 
    columns: [columnExample], 
    columnSettings: [columnSettingExample1, columnSettingExample2] }, // error! 
// ---------------------------------------> ~~~~~~~~~~~~~~~~~~~~~
// Types of property 'colId' are incompatible.
});

// const ColumnAndColumnSettings: ColumnAndColumnSettings<{ people: "1"; boats: "1"; }>

We get the error you were looking for!我们得到了您正在寻找的错误! The entry in T for boats is just "1" instead of "1" | "2" boatsT条目只是"1"而不是"1" | "2" "1" | "2" , and therefore the columnSettingExample2 element of the array is flagged as having an incompatible colId property. "1" | "2" ,因此数组的columnSettingExample2元素被标记为具有不兼容的colId属性。


Playground link to code 游乐场代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM