Reputation: 5015
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
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
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;
Upvotes: 3
Reputation: 11283
I tried your code on playground and indeed it complains about undefined
possibility.
But to your rescue you have some workarounds!
fieldCompany.form!.default
You can also disable strict null checks
tsconfig.json
{
"compilerOptions": {
"strictNullchecks": false
}
}
Upvotes: 0
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!
Upvotes: 2