是否可以将 typescript 对象限制为仅包含其类定义的属性?

Val*_*era 30 typescript

这是我的代码

async getAll(): Promise<GetAllUserData[]> {
    return await dbQuery(); // dbQuery returns User[]
}

class User {
    id: number;
    name: string;
}

class GetAllUserData{
    id: number;
}
Run Code Online (Sandbox Code Playgroud)

getAll函数返回User[],并且数组的每个元素都有name属性,即使它的返回类型是GetAllUserData[]

我想知道是否可以out of the box在打字稿中将对象限制为仅由其类型指定的属性。

Gre*_*egL 39

我想出了一种方法,使用自 TypeScript 版本 3 以来可用的内置类型,以确保传递给函数的对象不包含任何超出指定(对象)类型的属性。

// First, define a type that, when passed a union of keys, creates an object which 
// cannot have those properties. I couldn't find a way to use this type directly,
// but it can be used with the below type.
type Impossible<K extends keyof any> = {
  [P in K]: never;
};

// The secret sauce! Provide it the type that contains only the properties you want,
// and then a type that extends that type, based on what the caller provided
// using generics.
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;

// Now let's try it out!

// A simple type to work with
interface Animal {
  name: string;
  noise: string;
}

// This works, but I agree the type is pretty gross. But it might make it easier
// to see how this works.
//
// Whatever is passed to the function has to at least satisfy the Animal contract
// (the <T extends Animal> part), but then we intersect whatever type that is
// with an Impossible type which has only the keys on it that don't exist on Animal.
// The result is that the keys that don't exist on Animal have a type of `never`,
// so if they exist, they get flagged as an error!
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
  console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}

// This is the best I could reduce it to, using the NoExtraProperties<> type above.
// Functions which use this technique will need to all follow this formula.
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
  console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}

// It works for variables defined as the type
const okay: NoExtraProperties<Animal> = {
  name: 'Dog',
  noise: 'bark',
};

const wrong1: NoExtraProperties<Animal> = {
  name: 'Cat',
  noise: 'meow'
  betterThanDogs: false, // look, an error!
};

// What happens if we try to bypass the "Excess Properties Check" done on object literals
// by assigning it to a variable with no explicit type?
const wrong2 = {
  name: 'Rat',
  noise: 'squeak',
  idealScenarios: ['labs', 'storehouses'],
  invalid: true,
};

thisWorks(okay);
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above
thisWorks(wrong2); // yay, an error!

thisIsAsGoodAsICanGetIt(okay);
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
Run Code Online (Sandbox Code Playgroud)

  • 谢谢你!非常聪明的解决方案。您能否在许可下将此代码发布到 GitHub Gist,以便我可以自由地将其复制并粘贴到我的项目中? (2认同)
  • @likern 当然!我用 BSD 许可证将其放在[此处](https://gist.github.com/greglockwood/1610ef83d0726e0e6c021d46cb573e68)。但是[所有SO答案的许可证](https://gist.github.com/greglockwood/1610ef83d0726e0e6c021d46cb573e68)是“知识共享署名-相同方式共享”许可证,这是相当宽松的,只要你使用永久链接来归属代码将其复制并粘贴到您的项目中。 (2认同)
  • 这是一个聪明的解决方案,但它甚至不适用于所要求的示例。尝试将实用函数放入“Promise”中,其中 TS 从对象开始“类型推断”,您将看到 TS 会将子类型视为可替换。请阅读 https://medium.com/@lemoine.benoit/why-does-typescript-sometimes-fails-to-type-check-extra-properties-fd230ebbc295 了解更多详细信息,如下面“Robert Stiffler”的答案所指出的。 (2认同)
  • 这仅适用于对象文字;但无论如何,即使没有这种方法,对对象文字的过多属性检查仍然有效,至少在我检查过的 TS 4.1.3 中是这样。 (2认同)

Rob*_*ler 9

Typescript 使用结构类型而不是名义类型来确定类型相等。这意味着类型定义实际上只是该类型对象的“形状”。这也意味着任何共享另一种类型“形状”子集的类型都隐式地是该类型的子类。

在您的示例中,因为 aUser具有 的所有属性GetAllUserDataUser所以隐式是 的子类型GetAllUserData

为了解决这个问题,您可以专门添加一个虚拟属性,使您的两个类彼此不同。这种类型的属性称为鉴别器。(在这里搜索歧视工会)。

您的代码可能如下所示。鉴别器属性的名称并不重要。这样做会产生你想要的类型检查错误。

async function getAll(): Promise<GetAllUserData[]> {
  return await dbQuery(); // dbQuery returns User[]
}

class User {
  discriminator: 'User';
  id: number;
  name: string;
}

class GetAllUserData {
  discriminator: 'GetAllUserData';
  id: number;
}
Run Code Online (Sandbox Code Playgroud)

  • 看起来不错!但是,您是否应该在两者中都将“discriminator”属性设置为可选,以便我们在创建该类型的对象时实际上不必包含它? (2认同)
  • 对于您的用例,这应该没问题。还有其他用例(可区分联合),您希望能够在运行时判断对象的类型。对于这些情况,您可能希望需要鉴别器。 (2认同)

CRi*_*ice 8

我认为您拥有的代码结构是不可能的。Typescript确实多余的属性检查,这听起来像您所追求的,但它们仅适用于对象文字。从这些文档:

对象文字在将它们分配给其他变量或将它们作为参数传递时会得到特殊处理并进行额外的属性检查。

但是返回的变量不会经过该检查。所以虽然

function returnUserData(): GetAllUserData {
    return {id: 1, name: "John Doe"};
}
Run Code Online (Sandbox Code Playgroud)

会产生错误“Object literal may only specified known properties”,代码:

function returnUserData(): GetAllUserData {
    const user = {id: 1, name: "John Doe"};
    return user;
}
Run Code Online (Sandbox Code Playgroud)

不会产生任何错误,因为它返回一个变量而不是对象文字本身。

因此,对于您的情况,由于getAll不返回文字,因此打字稿不会进行多余的属性检查。

最后说明:“精确类型”存在一个问题,如果实施该问题将允许您在此处进行所需的检查。


max*_*992 6

GregL回答之后,我想添加对数组的支持,并确保如果你有一个数组,数组中的所有对象都没有额外的道具:

type Impossible<K extends keyof any> = {
  [P in K]: never;
};

export type NoExtraProperties<T, U extends T = T> = U extends Array<infer V>
  ? NoExtraProperties<V>[]
  : U & Impossible<Exclude<keyof U, keyof T>>;
Run Code Online (Sandbox Code Playgroud)

注意:只有当您拥有 TS 3.7(包含)或更高版本时,类型递归才可能。


Ben*_*arp 5

打字稿不能限制额外的属性

不幸的是,这在 Typescript 中目前是不可能的,并且与 TS 类型检查的形状性质有些矛盾。

此线程中基于泛型的答案NoExtraProperties非常优雅,但不幸的是它们不可靠,并且可能导致难以检测到错误。

我将用 GregL 的回答来证明。

// From GregL's answer

type Impossible<K extends keyof any> = {
    [P in K]: never;
 };

 type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;

 interface Animal {
    name: string;
    noise: string;
 }

 function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
    console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
 }

 function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
    console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
 }

 const wrong2 = {
    name: 'Rat',
    noise: 'squeak',
    idealScenarios: ['labs', 'storehouses'],
    invalid: true,
 };

 thisWorks(wrong2); // yay, an error!

 thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
Run Code Online (Sandbox Code Playgroud)

如果在将对象传递给thisWorks/ thisIsAsGoodAsICanGetTS 时识别出该对象具有额外的属性,则此方法有效。但在 TS 中,如果它不是一个对象字面量,一个值总是可以有额外的属性:

const fun = (animal:Animal) =>{
    thisWorks(animal) // No Error
    thisIsAsGoodAsICanGetIt(animal) // No Error
}

fun(wrong2) // No Error
Run Code Online (Sandbox Code Playgroud)

所以,在thisWorks/thisIsAsGoodAsICanGetIt你不能相信动物参数没有额外的属性。

解决方案

只需使用pick(Lodash、Ramda、Underscore)。

interface Narrow {
   a: "alpha"
}

interface Wide extends Narrow{
   b: "beta" 
}

const fun = (obj: Narrow) => {
   const narrowKeys = ["a"]
   const narrow = pick(obj, narrowKeys) 
   // Even if obj has extra properties, we know for sure that narrow doesn't
   ...
}
Run Code Online (Sandbox Code Playgroud)