To ensure that invalid states are not represented, consider using rest parameters instead of generics.
enum DataType { Foo = 'Foo', Bar = 'Bar' }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }
type MapStructure = {
[DataType.Foo]: FooData,
[DataType.Bar]: BarData
}
type Values<T> = T[keyof T]
type Tuple = {
[Prop in keyof MapStructure]: [type: Prop, data: MapStructure[Prop]]
}
// ---- > BE AWARE THAT IT WORKS ONLY IN T.S. 4.6 < -----
function func(...params: Values<Tuple>): void {
const [type, data] = params
const getter = <Data, Key extends keyof Data>(val: Data, key: Key) => val[key]
if (type === DataType.Bar) {
const foo = type
data; // BarData
console.log(data.otherKey) // ok
console.log(getter(data, 'otherKey')) // ok
console.log(getter(data, 'someKey')) // ok
}
}
Playground
MapStructure
- is used just for mapping keys with valid state.
Values<Tuple>
- creates a union of allowed tuples.Since rest parameters is nothing more than a tuple, it works like a charm.
Regarding getter
. You should either define it inside if
condition or make it separate function. SO, feel free to move getter
out of the scope of func
.
If you want to stick with generics, like in your original example, you should make type
and data
a part of one datastracture and then use typeguard
enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }
type Data<T extends DataType> = T extends DataType.Foo ? FooData : BarData
const isBar = (obj: { type: DataType, data: Data<DataType> }): obj is { type: DataType.Bar, data: BarData } => {
const { type, data } = obj;
return type === DataType.Bar && 'other' in data
}
function func<T extends DataType>(obj: { type: T, data: Data<T> }): void {
const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => obj.data[key]
if (isBar(obj)) {
obj.data // Data<T> & BarData
console.log(obj.data.otherKey) // ok
}
}
But issue with getter
still exists since it depend on uninfered obj.data
. You either need to move out getter
of func
scope and provide extra argument for data
or move getter
inside conditional statement
(not recommended).
However, you can switch to TypeScript nightly in TS playground and use object type for argument:
enum DataType { Foo, Bar }
interface FooData { someKey: string }
interface BarData extends FooData { otherKey: string }
type Data = { type: DataType.Foo, data: FooData } | { type: DataType.Bar, data: BarData }
function func(obj: Data): void {
const { type, data } = obj;
const getter = <K extends keyof typeof data>(key: K): typeof data[K] => data[key]
if (type === DataType.Bar) {
data // BarData
console.log(obj.data.otherKey) // ok
}
}
Playground
getter
still does not work in a way you expect, hence I recomment to move it out from func