I'd like to constrain an object to be of a certain type, but also to cast it "as const", so that certain properties could be typed literally. However, when I use "as const" with a type definition as in the code below, the inferred type isn't literal - "as const" is ignored.
interface IFilterBase {
type: string
...
}
const COST_FILTER: IFilterBase = {
type: "cost",
...
} as const
In the code above, "as const" is ignored. COST_FILTER.type
is inferred as a string
, not as "cost"
.
Is there a way to constrain COST_FILTER
to be implement IFilterBase
type, yet for it's properties to be inferred "as const"?
If you specify the type explicitly typescript will only check for compatibility with the interface. There is a proposal as outlined in comments to support this in the language.
Until that happens, we can play around with the inference rules for a functions and tuples and literals:
[unknown] | unknown[]
.With these rules, we can just create a recursive mapped type to map the properties of the original type to a new type that contains such generic type parameters. We don't separate type parameters for every property, one parameter will do for literals and one for tuples. This is just enough to hint to the compiler what we want.
type WithLiterals<T, L, LTuple> =
T extends string| number | boolean | null | undefined ? T & L :
{
[P in keyof T]:
WithLiterals<T[P], L, LTuple> & (T[P] extends Array<any> ? LTuple: unknown)
}
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}
function asConst<TInterface>()
{
return function<
LTuple extends [unknown] | unknown[],
L extends string | boolean | number, T extends WithLiterals<TInterface, L, LTuple>>(o: T): DeepReadonly<T> {
return o as any
}
}
type IFilterBase = {
type: "cost" | "other",
displayName: string | undefined,
nr: number,
nrUnion: 1 | 2,
subObj : {
a: string;
}
arr: string[]
larr: ("A" | "B")[]
mixedarr: (number | string)[],
oArray: Array<{
a: string
}>
}
export const COST_FILTER = asConst<IFilterBase>()({
type: "other",
nr: 1,
nrUnion: 1,
displayName: "Cost",
subObj: {
a: "A"
},
arr: ["A", "B"],
larr: ["A"],
mixedarr: [1, ""],
oArray: [
{ a: ""}
]
})
Typed as :
export const COST_FILTER : DeepReadonly<{
type: "other";
nr: 1;
nrUnion: 1;
displayName: "Cost";
subObj: {
a: "A";
};
arr: ["A", "B"];
larr: ["A"];
mixedarr: [1, ""];
oArray: [{
a: "";
}];
}>
Link
This could be achieved using a dummy validating function.
const validateType = <T> (obj:T) => undefined
All that is left is to call it with the type and object:
const COST_FILTER: IFilterBase = {
type: "cost",
displayName: "Cost",
fields: costFilterFields,
} as const
validateType<FilterBase>(COST_FILTER) // Will show an error if types don't match.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With