You need to make sure that invalid state is unrepresentable. You can use rest parameters instead of generic.
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