I'm going to presume that the propertyType property will be a string that describes the type, like "boolean" or "date". If so, then for this to work you'll need to keep a type that maps from these strings to the TypeScript types they represent:
interface TypeMap {
boolean: boolean;
number: number;
string: string;
date: Date;
// add any name-to-type mappings you need here
}
and then we can say that your SurveyQuestion type needs to use on of the keys of TypeMap as propertyType:
interface SurveyQuestion {
propertyName: string,
propertyType: keyof TypeMap,
}
When you define question, you don't want to annotate it as SurveyQuestion[]. You need the compiler to keep track of the particular propertyName and propertyType values inside the initializing array literal, but if you annotate the variable property as SurveyQuestion[] the compiler will forget all those details. So you want to write const questions = ... and not const questions: SurveyQuestion[] = ....
And even that's not quite enough; the compiler won't realize you care about the literal types of those property values unless you tell it so. Normally a value like [{a: "abc", x: "xyz"}, {a: "def", x: "uvw"}] will be inferred as type Array<{a: string, x: string}>, even without an annotation. It's more common for people to want a type like that then something very specific about "abc" and "xyz". But you want the most specific type possible, so you can do so with a const assertion:
const questions = [
{ propertyName: "isNewUser", propertyType: "boolean" },
{ propertyName: "satisfactionScore", propertyType: "number" },
{ propretyName: "comments", propertyType: "string" },
] as const;
And IntelliSense shows you that the type of questions is:
/* const questions: readonly [{
readonly propertyName: "isNewUser";
readonly propertyType: "boolean";
}, {
readonly propertyName: "satisfactionScore";
readonly propertyType: "number";
}, {
readonly propretyName: "comments";
readonly propertyType: "string";
}] */
So that's enough information to work with now, hooray!
But note that we haven't used SurveyQuestion at all. The compiler didn't realize that we wanted SurveyQuestions and so it didn't catch that I wrote propretyName instead of propertyName in there. Oops!
To fix that, you can use the new satisfies operator to have the compiler check that your initializer is an array of SurveyQuestions without widening the variable to that type:
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>;
Now the compiler has caught the error and we can fix it:
const questions = [
{ propertyName: "isNewUser", propertyType: "boolean" },
{ propertyName: "satisfactionScore", propertyType: "number" },
{ propertyName: "comments", propertyType: "string" },
// { propertyName: "anotherProp", propertyType: "date" }
] as const satisfies ReadonlyArray<SurveyQuestion>;
Okay, great.
Now all we have to do is define SurveyWithQuestions in terms of questions. First we can get the type of questions using the TypeScript typeof type operator, and index into it with number to get the union if its element types:
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";
} */
And now we can 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;
} */
And finally, we can extend it with any other "static" properties to get SurveyQuestions:
interface SurveyWithQuestions extends ResponseFormat {
date: Date;
username: string;
}
Let's test it out:
let survey1: SurveyWithQuestions = {
date: new Date("1/1/2023"),
username: "johndoe",
isNewUser: true,
satisfactionScore: 5,
comments: "happy user",
}; // okay
So that works as desired. Let's see what happens if we add a line to our questions definition:
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",
};
// Property 'dateOfBirth' is missing in type ...
// but required in type 'SurveyWithQuestions'.
So the compiler now expects a dateOfBirth property to exist. Let's give it one:
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'.
};
Oops, wrong type:
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
Looks good!
Playground link to code
typeof Booleanandtypeof Numberandtypeof Stringare almost certainly not what you want; those are just the string"function". AndObjectis also almost certainly not what you want, since that matches any non-nullish value, including the string"function", which is why that's not an error. Could you please edit the code to be a minimal reproducible example that actually demonstrates what you're trying to do, at least the runtime code? Then we can help you give it typings.