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

时间:2018-03-30 20:03:57

标签: typescript

这是我的代码

Read

async getAll(): Promise<GetAllUserData[]> { return await dbQuery(); // dbQuery returns User[] } class User { id: number; name: string; } class GetAllUserData{ id: number; } 函数返回getAll,并且数组的每个元素都有User[]属性,如果是&#,则为# 39; s返回类型是name

我想知道在typescript中是否可以将GetAllUserData[]限制为仅将对象限制为由其类型指定的属性。

6 个答案:

答案 0 :(得分:3)

紧跟GregLanswer之后,我想添加对数组的支持,并确保如果您拥有一个,则数组中的所有对象都没有多余的道具:

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>>;

注意:仅当您拥有TS 3.7(包括)或更高版本时,才可以进行类型递归。

答案 1 :(得分:3)

Typescript不能限制额外的属性

不幸的是,这在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!

如果在将对象传递到thisWorks / thisIsAsGoodAsICanGet时TS识别出该对象具有其他属性,则此方法有效。但是在TS中,如果它不是对象文字,则值始终可以具有额外的属性:

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

fun(wrong2) // No Error

因此,在thisWorks / thisIsAsGoodAsICanGetIt中,您不能相信动物参数没有额外的属性。

解决方案

只需使用选择(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
   ...
}

答案 2 :(得分:2)

Typescript uses structural typing instead of nominal typing to determine type equality.这意味着类型定义实际上只是该类型对象的“形状”。它还意味着任何共享另一个类型的“形状”子集的类型都隐式地是该类型的子类。

在您的示例中,由于User具有GetAllUserData的所有属性,User隐式地是GetAllUserData的子类型。

要解决此问题,您可以专门添加一个虚拟属性,以使两个类彼此不同。这种类型的属性称为鉴别器。 (搜索有区别的联盟here)。

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

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;
}

答案 3 :(得分:1)

我认为你的代码结构不可能。 Typescript 确实excess property checks,这听起来像你所追求的,但它们只适用于对象文字。从那些文档:

  

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

但是返回的变量不会进行检查。所以,而

function returnUserData(): GetAllUserData {
    return {id: 1, name: "John Doe"};
}

会产生错误“对象文字只能指定已知属性”,代码为:

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

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

因此,对于您的情况,由于getAll未返回文字,因此typescript将不会执行多余的属性检查。

最后注意:有一个issue for "Exact Types"如果实施的话会允许您在此处进行检查。

答案 4 :(得分:0)

接受了歧视者的答案是正确的。 TypeScript使用结构类型而不是标称类型。这意味着转换器将检查结构是否匹配。由于这两个类(可以是接口或类型)的id类型number匹配,因此可以互换(因为User具有更多属性,所以这是正确的。

虽然这可能足够好,但问题是在运行时,方法getAll返回的数据将包含name属性。返回更多可能不是问题,但如果您将信息发送回其他地方,可能就是这样。

如果要将数据限制为仅在类(接口或类型)中定义的内容,则必须手动构建或传播新对象。以下是它如何查找您的示例:

function dbQuery(): User[] {
    return [];
}
function getAll(): GetAllUserData[] {
    const users: User[] = dbQuery();
    const usersIDs: GetAllUserData[] = users.map(({id}) => ({id}));
    return usersIDs;
}

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

class GetAllUserData {
    id: number;
}

不使用修剪字段的运行时方法,您可以向TypeScript指出两个类与私有字段不同。当返回类型设置为User

时,以下代码不允许您返回GetAllUserData
class User {

    id: number;
    name: string;
}

class GetAllUserData {
    private _unique: void;
    id: number;
}
function getAll(): GetAllUserData[] {
    return dbQuery(); // Doesn't compile here!
}

答案 5 :(得分:0)

我想出了一种方法,使用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!