Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

As const is ignored when there is a type definition

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"?

like image 858
Ben Carp Avatar asked Jul 17 '19 06:07

Ben Carp


2 Answers

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:

  • A literal type will be inferred (or better said preserved) if it is assigned in a position that is typed as a generic type parameter constrained to a type that can have literals
  • A tuple will be inferred if an array is assigned to a position that is typed as a generic type parameter constrained to [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

like image 70
Titian Cernicova-Dragomir Avatar answered Sep 20 '22 10:09

Titian Cernicova-Dragomir


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. 
like image 44
Ben Carp Avatar answered Sep 18 '22 10:09

Ben Carp