0

I have a class Form that has this signature:

interface IFormSchema {
  name: string;
  // Removed irrelevant fields...
}

const schema: IFormSchema[] = [{ name: 'firstName' }];

// Form Interface
interface IForm {
  fields: IFields;
  // Removed irrelevant fields...
}

// Here is the IFields interface used in IForm
interface IFields {
  // The key here is what I want typed **************
  [key: string]: IField;
}

const form: IForm = new Form(schema: IFormSchema[]);

The schema array is iterated and each object is converted to a Field with the IField interface:


interface IField {
  form: IForm;
  name: string;
  // Removed irrelevant fields...
}

Now, when I new up the Form and then I access the Form.fields, and can access the field by its name like so form.fields.firstName, I want firstName to be typed so that if I try and access form.fields.wrongFieldName, TypeScript will throw an error.

How would I do this? Any help would be greatly appreciated.

dericcain
  • 2,182
  • 7
  • 30
  • 52

3 Answers3

3

This is possible, but you need to use generic types and type inference. If you have an explicit type annotation like : IForm then there's nothing to allow one IForm to have a firstName field while another IForm lacks that field.

type IFormSchema<K extends string> = { name: K }
type IFields<K extends string> = Record<K, string>

class Form<K extends string> {
  public fields: IFields<K>;
  constructor(schema: readonly IFormSchema<K>[]) {
    // initialise this.fields here
    throw new Error('Not implemented');
  }
}

Example usage:

// no type annotation, and must use `as const` to infer string literal types
const schema = [{name: 'firstName'}, {name: 'lastName'}] as const;
// no type annotation; allow the type argument K to be inferred
const form = new Form(schema);

let ok1: string = form.fields.firstName;
let ok2: string = form.fields.lastName;
let error: string = form.fields.address; // property 'address' does not exist

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • Man, I think you are on to something here. The problem that I have now is that when I try and access the `fields` dynamically, e.g., `this.fields[name]`, I get the error `No index signature with a parameter of type 'string' was found on type 'Record'`. Here is a codesandbox link: https://codesandbox.io/s/yiklo – dericcain Mar 06 '20 at 14:28
  • 1
    Change the return type of `getFieldNames` from `string[]` to `K[]` - then its elements are of type `K` and you can use them as keys in `this.fields`. You will need a type assertion in `return Object.keys(this.fields) as K[];` to make that work. After that you need to declare `isTouched`, `isValid` and `isFocused` in the interface `IField`, and all the errors are gone. https://codesandbox.io/s/typescript-playground-export-r4i6f?fontsize=14&hidenavigation=1&theme=dark – kaya3 Mar 06 '20 at 14:42
0

It Isn't Possible. Unless you define the fields in a separate Record Type to define the [key: string] as an Enum, or in a RecordsType.

enum FieldsEnum = {
  FIRST_NAME = 'firstName',
  LAST_NAME = 'lastName',
};

type BaseFieldsType = {
 // other fields here 
 // and then the below 
 [key in FieldsEnum]?: IField;
};

interface IFields extends BaseFieldsType {}; 
DesTroy
  • 390
  • 4
  • 17
  • Okay, I have been working on trying to get this for some time and just can't figure it out. The hard part is that the `name` value is defined at the time of construction, so the Field has no way of knowing ahead of time what it could be. The value would have to be pulled from the array of objects that are passed in. – dericcain Mar 06 '20 at 14:05
  • 1
    you should defer from using "It isn't possible" statements, the fact you are not aware of a solution does not mean it is impossible. look at @kaya3's suggestion – DoronG Mar 09 '20 at 16:40
  • @DoronG, my "It isn't Possible" also came with an "Unless". – DesTroy Mar 10 '20 at 09:46
-1

You just need to specify it in the keys for IFields:

interface IFields {
  firstName: IField;
}

In fact, if you are sure of what keys will be available inside of IFields, you can get rid of the index signature and just use keys.

night_owl
  • 856
  • 6
  • 12
  • I don't know ahead of time what the `Field` names are going to be, so this isn't feasible unless I am misunderstanding what you're saying. – dericcain Mar 06 '20 at 14:06
  • Ah okay, I see now. You may be able to put something together using type guards. See https://stackoverflow.com/a/44078574/1433116 – night_owl Mar 06 '20 at 14:17