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

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属性将是描述类型的字符串,如"boolean""date".如果是这样的话,您需要保留一个从这些字符串映射到它们所代表的类型的类型:

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

然后我们可以说,您的SurveyQuestion类型需要使用TypeMap中的一个键作为propertyType:

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

当你定义question的时候,你不会想把它定义为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组成的数组,而不会将变量扩展到该类型:

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

好的,太好了.


现在我们要做的就是用questions来定义SurveyWithQuestions.首先,我们可以使用the TypeScript typeof type operator获取questions的类型,并使用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",
}; 

//类型中缺少属性""ateOfBirth""... //但在‘SurveyWithQuestions’类型中是必需的.

因此,编译器现在希望存在一个dateOfBirth属性.让我们给它一个:

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

看上go 不错!

Playground link to code

Typescript相关问答推荐

如何使用Generics保留构造函数的类型信息?

当我点击外部按钮时,如何打开Html Select 选项菜单?

Redux—Toolkit查询仅在不为空的情况下添加参数

分解对象时出现打字错误

使用KeyOf和Never的循环引用

转换器不需要的类型交集

如何缩小函数的返回类型?

跟踪深度路径时按条件提取嵌套类型

在Cypress中,您能找到表格中具有特定文本的第一行吗?

TS2739将接口类型变量赋值给具体变量

抽象类对派生类强制不同的构造函数签名

Typescript将键数组映射到属性数组

在Thunk中间件中输入额外参数时Redux工具包的打字脚本错误

有没有办法在Reaction组件中动态导入打字脚本`type`?

窄SomeType与SomeType[]

有没有一种方法可以防止我的组件包在其中安装样式组件'; node 模块中的s目录

TypeScript:方法获胜';t接受较窄类型作为参数

Typescript 从泛型推断类型

Typescript 确保通用对象不包含属性

在Typescript 中,是否有一种方法来定义一个类型,其中该类型是字符串子集的所有可能组合?