Reputation: 389
I'm wondering if it is possible to use type guards or in any other clever determine what kind of type I'm getting out of a function, since all TypeA-objects are marked with Type: "TypeA" as well as is inside a "TypeA"-object. The statement I want to avoid type casting is in the final line of this rather long code block:
type ControlPageType = "TypeA" | "TypeB";
interface ControlPageDto {
type: ControlPageType;
}
interface TypeAData extends ControlPageDto {
foo: string;
}
interface TypeBData extends ControlPageDto {
bar: number;
}
export interface ControlPageState<T extends ControlPageDto> {
readonly type: ControlPageType;
readonly checksum: string;
readonly backendState: T;
}
export interface TypeA extends ControlPageState<TypeAData> {
type: "TypeA";
}
export interface TypeB extends ControlPageState<TypeAData> {
type: "TypeB";
}
export interface ControllerState<D extends ControlPageDto, S extends ControlPageState<D>> {
pages: S[];
}
export interface ApplicationState {
controllers: { [K in ControlPageType]: ControllerState<any, any> };
currentChecksum: string;
}
export const findCurrentPage = (state: ApplicationState, type: ControlPageType): ControlPageState<any> => {
const checksum = state.currentChecksum;
if (checksum === null) {
return null;
}
const controllerState = state.controllers[type];
const pages = controllerState.pages;
for (const bilde of pages) {
if (bilde.checksum === checksum) {
return bilde;
}
}
return null;
};
const usage = (): void => {
const sampleA1: TypeAData = { type: "TypeA", foo: "FOO" };
const sampleA2: TypeAData = { type: "TypeA", foo: "OOF" };
const sampleB1: TypeBData = { type: "TypeB", bar: 123 };
const sampleData: ApplicationState = {
controllers: {
TypeA: {
pages: [{ type: "TypeA", checksum: "1", customData: sampleA1 }, { type: "TypeA", checksum: "2", customData: sampleA2 }]
},
TypeB: { pages: [{ type: "TypeB", checksum: "A", customData: sampleB1 }] }
},
currentChecksum: "1"
};
const page = findCurrentPage(sampleData, "TypeA") as TypeA; // TODO Avoid type casting
};
Upvotes: 1
Views: 2186
Reputation: 249536
There could be several ways to do this.
The simplest way is to provide several overloads for each constant:
export function findCurrentPage(state: ApplicationState, type: 'TypeA'): TypeA
export function findCurrentPage(state: ApplicationState, type: 'TypeB'): TypeB
export function findCurrentPage(state: ApplicationState, type: ControlPageType): ControlPageState<any> {
//...
return null;
};
This has the disadvantage of having to define an overload for each, and you will have to maintain the types in several places, both ControlPageType
and the function.
A more DRY solution, would be to define an object type that maps the string to the type and use that to get the type base on the keys of the map:
type ControlPageTypeMap = {
"TypeA": TypeA
"TypeB": TypeB
};
type ControlPageType = keyof ControlPageTypeMap;
export function findCurrentPage<K extends ControlPageType>(state: ApplicationState, type: K): ControlPageTypeMap[K] {
//...
return null;
};
Or if you want to be extra safe and avoid cases where someone mistakenly maps "TypeA": TypeB
, you can declare a helper function that constrains the mapping to be valid. The helper function and the helper variable are just there to help with type inference, there is no runtime behavior associated with them.
// Helper to constrain each entry in the map type to have a property named type that has the same type P as the current key
function typeMapValidator<T extends { [P in keyof T] : { type: P } }>() : T{return null;}
// Helper will be null at runtime
let mapHelper = typeMapValidator<{
"TypeA": TypeA
"TypeB": TypeB
"TypeC": TypeA // Will be an error
}>()
type ControlPageTypeMap = typeof mapHelper;
type ControlPageType = keyof ControlPageTypeMap;
export function findCurrentPage<K extends ControlPageType>(state: ApplicationState, type: K): ControlPageTypeMap[K] {
//...
return null;
};
Upvotes: 1