typescript - 动态 Typescript 对象属性

标签 typescript typescript-generics

我想动态扩展一个类型,以便我可以执行以下操作:

interface SurveyQuestion {
  propertyName: string,
  propertyType: Object,
}

const questions: SurveyQuestion[] = [
  { propertyName: "isNewUser", propertyType: typeof Boolean },
  { propertyName: "satisfactionScore", propertyType: typeof Number },
  { propertyName: "comments", propertyType: typeof String },
];

type SurveyWithQuestions = {
  date: Date;
  username: string;
}

let survey1: SurveyWithQuestions = { 
  date: new Date("1/1/2023"),
  username: "johndoe",
  isNewUser: true,
  satisfactionScore: 5,
  comments: "happy user"
};

...这样添加的新问题就不会出错,并且允许完成代码。 This使用泛型和对象键来接近。但我无法正确修改它来迭代问题。

最佳答案

我假设 propertyType property 将是描述类型的字符串,例如 "boolean""date" 。如果是这样,那么为了使其工作,您需要保留一个从这些字符串映射到它们代表的 TypeScript 类型的类型:

interface TypeMap {
    boolean: boolean;
    number: number;
    string: string;
    date: Date;
    // add any name-to-type mappings you need here
}

然后我们可以说你的 SurveyQuestion type 需要使用 TypeMap 中的一个键如propertyType :

interface SurveyQuestion {
    propertyName: string,
    propertyType: keyof TypeMap,
}

当您定义question时,你不想annotate其为SurveyQuestion[] 。您需要编译器跟踪特定的 propertyNamepropertyType初始化数组文字内的值,但如果将变量属性注释为 SurveyQuestion[]编译器会忘记所有这些细节。所以你想写 const questions = ...而不是const questions: SurveyQuestion[] = ... .

即使这样还不够;编译器不会意识到你关心 literal types除非你这么说,否则这些属性值。通常是类似 [{a: "abc", x: "xyz"}, {a: "def", x: "uvw"}] 的值将被推断为类型 Array<{a: string, x: string}> ,即使没有注释。人们更常见的是想要这样的类型,然后是关于 "abc" 的非常具体的类型。和"xyz" 。但您想要尽可能具体的类型,因此您可以使用 const assertion 来实现。 :

const questions = [
    { propertyName: "isNewUser", propertyType: "boolean" },
    { propertyName: "satisfactionScore", propertyType: "number" },
    { propretyName: "comments", propertyType: "string" },
] as const;

IntelliSense 会向您显示 questions 的类型是:

/* const questions: readonly [{
    readonly propertyName: "isNewUser";
    readonly propertyType: "boolean";
}, {
    readonly propertyName: "satisfactionScore";
    readonly propertyType: "number";
}, {
    readonly propretyName: "comments";
    readonly propertyType: "string";
}] */

现在就可以使用足够的信息了,万岁!


但请注意,我们没有使用 SurveyQuestion根本不。编译器没有意识到我们想要 SurveyQuestion所以它没有发现我写了 propretyName而不是propertyName在那里。哎呀!

要解决这个问题,您可以使用新的 satisfies operator让编译器检查您的初始值设定项是否为 SurveyQuestion 的数组s 而不将变量扩展为该类型:

const questions = [
    { propertyName: "isNewUser", propertyType: "boolean" },
    { propertyName: "satisfactionScore", propertyType: "number" },
    { propretyName: "comments", propertyType: "string" }, // error!
//    ~~~~~~~~~~~~~~~~~~~~~~~ <-- Did you mean to write 'propertyName'?
] as const satisfies ReadonlyArray<SurveyQuestion>;

现在编译器已经捕获了错误,我们可以修复它:

const questions = [
    { propertyName: "isNewUser", propertyType: "boolean" },
    { propertyName: "satisfactionScore", propertyType: "number" },
    { propertyName: "comments", propertyType: "string" },
    // { propertyName: "anotherProp", propertyType: "date" }
] as const satisfies ReadonlyArray<SurveyQuestion>;

好的,太好了。


现在我们要做的就是定义 SurveyWithQuestionsquestions而言。首先我们可以得到questions的类型使用the TypeScript typeof type operator ,并用 number 对其进行索引获取union如果其元素类型:

type QuestionType = typeof questions[number];

/* type QuestionType = {
    readonly propertyName: "isNewUser";
    readonly propertyType: "boolean";
} | {
    readonly propertyName: "satisfactionScore";
    readonly propertyType: "number";
} | {
    readonly propertyName: "comments";
    readonly propertyType: "string";
} */

现在我们可以iterate over that union and remap it to object properties :

type ResponseFormat = {
    [T in QuestionType as T["propertyName"]]:
    TypeMap[T["propertyType"]]
}

/* type ResponseFormat = {
    isNewUser: boolean;
    satisfactionScore: number;
    comments: string;
} */

最后,我们可以使用任何其他“静态”属性来扩展它以获得 SurveyQuestions :

interface SurveyWithQuestions extends ResponseFormat {
    date: Date;
    username: string;
}

让我们测试一下:

let survey1: SurveyWithQuestions = {
    date: new Date("1/1/2023"),
    username: "johndoe",
    isNewUser: true,
    satisfactionScore: 5,
    comments: "happy user",
}; // okay

这样就可以按预期工作了。让我们看看如果我们向 questions 添加一行会发生什么定义:

const questions = [
    { propertyName: "isNewUser", propertyType: "boolean" },
    { propertyName: "satisfactionScore", propertyType: "number" },
    { propertyName: "comments", propertyType: "string" },
    { propertyName: "dateOfBirth", propertyType: "date" } // <-- add this
] as const satisfies ReadonlyArray<SurveyQuestion>;

let survey1: SurveyWithQuestions = { // error!
    date: new Date("1/1/2023"),
    username: "johndoe",
    isNewUser: true,
    satisfactionScore: 5,
    comments: "happy user",
}; 

//类型中缺少属性“dateOfBirth”... //但在“SurveyWithQuestions”类型中是必需的。

所以编译器现在期望 dateOfBirth属性(property)的存在。让我们举一个:

let survey1: SurveyWithQuestions = {
    date: new Date("1/1/2023"),
    username: "johndoe",
    isNewUser: true,
    satisfactionScore: 5,
    comments: "happy user",
    dateOfBirth: "yesterday" // error! 
    // Type 'string' is not assignable to type 'Date'.
};

糟糕,类型错误:

let survey1: SurveyWithQuestions = {
    date: new Date("1/1/2023"),
    username: "johndoe",
    isNewUser: true,
    satisfactionScore: 5,
    comments: "happy user",
    dateOfBirth: new Date(Date.now() - 8.64e7)
}; // okay

看起来不错!

Playground link to code

关于typescript - 动态 Typescript 对象属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/75707569/

相关文章:

javascript - React-Typescript,在单独的文件中或在同一文件中声明接口(interface),哪个更好?

带有通用类型的 Typescript 装饰器

typescript :将对象属性和嵌套对象属性的类型更改为一种类型

来自接口(interface)实现的 Typescript 泛型推断

arrays - 检查如何访问 Angular 5 HTML 中的数组元素

javascript - Angular 2+ : Opening Bootstrap modal through typescript

typescript - 在受约束的通用函数中混淆 "[ts] Type ... is not assignable to type [2322]"错误

typescript - 扩展 'generic' TypeScript 接口(interface)

Angular 2 在没有模拟的情况下测试具有可观察对象的组件

Angular 15 - 考虑使用 @Inject 装饰器来指定注入(inject) token