打字稿:声明对象上的所有属性必须属于同一类型

Typescript: declare that ALL properties on an object must be of the same type

在 Typescript 中,您可以像这样声明数组中的所有元素都是同一类型:

const theArray: MyInterface[]

您是否可以做类似的事情来声明一个对象的所有 属性 值必须属于同一类型? (不指定每个 属性 名称)

例如,我目前正在这样做:

interface MyInterface {
    name:string;
}

const allTheThingsCurrently = {
    first: <MyInterface>{name: 'first thing name' },
    second: <MyInterface>{name: 'second thing name' },
    third: <MyInterface>{name: 'third thing name' },
    //...
};

...请注意我必须如何为每个 属性 指定 <MyInterface>。有什么捷径吗?即我正在想象这样的事情......

const allTheThingsWanted:MyInterface{} = {
    first: {name: 'first thing name' },
    second: {name: 'second thing name' },
    third: {name: 'third thing name' },
    //...
};

MyInterface{} 是无效代码的部分,我正在寻找一种方法来减少冗余,并且可以选择额外的严格性来防止任何其他属性被添加到不同类型的对象中。

解决方案 1:Indexable type

interface Thing {
  name: string
}

interface ThingMap {
  [thingName: string]: Thing
}

const allTheThings: ThingMap = {
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
}

缺点是您可以访问 allTheThings 中的任何 属性 而不会出现任何错误:

allTheThings.nonexistent // type is Thing

这可以通过将 ThingMap 定义为 [thingName: string]: Thing | void 来变得更安全,但这将需要在所有地方进行空检查,即使您正在访问您知道的 属性 .

解决方案 2:具有空操作函数的泛型

const createThings = <M extends ThingMap>(things: M) => things

const allTheThings = createThings({
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
  fourth: { oops: 'lol!' }, // error here
})

allTheThings.first
allTheThings.nonexistent // comment out "fourth" above, error here

createThings函数有一个通用的MM可以是任何东西,只要所有的值都是Thing,那么它returns M。当你传入一个对象时,它会根据 extends 之后的类型验证该对象,同时返回与你传入的相同的形状。

这是 "smartest" 解决方案,但使用了一些看起来很聪明的 hack 来实际运行。无论如何,在 TS 添加更好的模式来支持这种情况之前,这将是我的首选路线。

使用泛型并指定您想要的属性类型。

type SamePropTypeOnly<T> = {
  [P: string]: T;
}

interface MyInterface {
  name: string;
}

const newObj: SamePropTypeOnly<MyInterface> = {
  first: { name: 'first thing name' },
  second: { name: 'second thing name' },
  third: { name: 'third thing name' },
  // forth: 'Blah' // Type 'string' is not assignable to type `MyInterface`
}

newObj.not_there; // undefined - no error

注意: 如果必须限制 属性 名称列表,则必须明确指定键:

interface MyInterface {
  name: string;
}

type OptionKeys = 'first' | 'second' | 'third';

const newObj: Record<OptionKeys, MyInterface> = {
  first: { name: 'first thing name' },
  second: { name: 'second thing name' },
  third: { name: 'third thing name' },
  // forth: 'Blah' // error
}

newObj.not_there // Property 'not_there' does not exist on type...

单层(平面)对象的一些替代方案:

备选方案 1 (indexable type):

const exampleObj: { [k: string]: string } = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}

备选方案 2 (Record):

const exampleObj: Record<string, string> = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}

备选方案 3(Record / 联盟):

const exampleObj: Record<'first' | 'second' | 'third', string> = {
  first: 'premier',
  second: 'deuxieme',
  third: 'troisieme',
}

方法 可以扩展为具有接受一种类型的所需值的通用函数,它本身 returns 无操作函数。 这样就不需要为每种类型创建新函数

export const typedRecord = <TValue>() => <T extends Record<PropertyKey, TValue>>(v: T): T => v;

要了解下面发生的事情,请参阅上面的 typedRecord 函数的替代声明。 typedRecord 函数接受 属性 记录类型的类型参数 TValue 和 returns 另一个函数,该函数将用于验证传递给 T 类型的结构它(TS 编译器将从调用中推断 T

export function typedRecord<TValue>() {
  return function identityFunction<T extends Record<PropertyKey, TValue>>(v: T): T {
    return v;
  };
}

这涵盖了所有要求

const allTheThings = typedRecord<Thing>()({
  first: { name: "first thing name" },
  second: { name: "second thing name" },
  third: { name: "third thing name" },
  fourth: { oops: "lol!" }, // error here
});

allTheThings.first;
allTheThings.nonexistent; // error here