japrescott
japrescott

Reputation: 5015

make optional prop in interface on implementation not optional

assume we have an interface IFieldConfig

export interface IFieldInputConfig {
    type: 'string' | 'password';
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    default?: any;
}

export interface IFieldConfig extends IExtractConfig {
    id: string;
    form?: IFieldInputConfig;
}

and have an object that uses that interface

export const fieldCompany: IFieldConfig = {
    id: 'company',
    form: {
        type: 'string',
        default: 'Apple',
    },
}

when I want to consume this, TypeScript thinks fieldCompany.form.default could be undefined, even if it's very clear that it is defined. I understand, that after the declaration somehow the form prop could be deleted so TypeScript is not wrong.

How can I tell TypeScript this is sealed/wont be modified?

Obviously I could make more specific interfaces but I don't want to end up with one for every combination of optional props. I am sure there is an easier and more straightforward way

Upvotes: 0

Views: 339

Answers (4)

Mx.Wolf
Mx.Wolf

Reputation: 678

You may define a generic DeepRequired to get something like this:

  interface IExtractConfig{}

  export interface IFieldInputConfig<L extends any = any> {
    type: 'string' | 'password';
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    default?: L;
  }

  export interface IFieldConfig<L extends any = any> extends IExtractConfig {
    id: string;
    form?: IFieldInputConfig<L>;
  }

  type DeepRequired<T> = Required<{
    [K in keyof T]: Required<DeepRequired<T[K]>>
  }>

  export const fieldCompany: DeepRequired<IFieldConfig<string>> = {
    id: 'company',
    form: {
        type: 'string',
        default: 'Apple',
    },
  }

Typescript would not be happy with default:any field. therefore I change it to generic parameter.

Now you have it fieldCompany.form.default has "string"

  function user(fc:DeepRequired<IFieldConfig<string>>){
    //here         fc.form.default assumed to be "string" and not "string" | "undefined" 
  }

Some time you have to have the base interface with optional properties. But from my experience using all properties as mandatory is better. Latter you can use Partial and Pick to cut the original shape in pieces.

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1073968

I may be being a bit simplistic, but one solution that jumps out is just not to say it's an IFieldConfig at all:

export const fieldCompany = {
  id: 'company',
  form: {
    type: 'string' as const, // <=== Note
    default: 'Apple',
  },
};

Now, fieldCompany.form.default is type string, not string | undefined. You need the as const on type: 'string' because otherwise it will be inferred as string, not 'string' | 'password'.

The object is still assignment compatible with IFieldConfig; this works:

const x: IFieldConfig = fieldCompany;

On the playground

Upvotes: 3

J&#243;zef Podlecki
J&#243;zef Podlecki

Reputation: 11283

I tried your code on playground and indeed it complains about undefined possibility.

But to your rescue you have some workarounds!

Non-null assertion operator

fieldCompany.form!.default

You can also disable strict null checks

tsconfig.json

{
  "compilerOptions": {
    "strictNullchecks": false
  }
}

Upvotes: 0

jcalz
jcalz

Reputation: 327624

Generally speaking I wouldn't annotate a variable with a type unless I want the compiler to "forget" the specifics of the value being assigned. Only union-typed values will get narrowed on assignment, and IFieldConfig is not itself a union type. So when you assign a value to a variable of type IFieldConfig, the type of the variable is not narrowed via control flow analysis to anything more specific. See microsoft/TypeScript#16976 and microsoft/TypeScript#27706 for more information.

My suggestion here (without more info to lead me to believe otherwise) is to leave off the annotation of fieldCompany, and use a const assertion to keep the inferred type as narrow as possible:

const fieldCompany = {
  id: 'company',
  form: {
    type: 'string',
    default: 'Apple',
  },
} as const;

You can tweak this if you want (e.g., maybe only the type filed of form should be as const), depending on which parts of the structure you intend to allow to change and which parts should stay narrow/fixed.

Since TypeScript's type system is structural and not nominal, this should not prevent you from using fieldCompany as an IFieldConfig:

function acceptIFieldConfig(c: IFieldConfig) { }
acceptIFieldConfig(fieldCompany); // okay

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

Related Questions