I've got a function that accepts a an enum value as T
and a generic type Data<T>
that chooses between two data types.
I hoped to be able to access properties of type BarData
inside a conditional that should make T
known. However, it still reads data
as a union type.
The code works as expected but what do I have to change to get rid of the typescript errors?
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
function func<T extends DataType>(type: T, data: Data<T>): void {
const getter = <K extends keyof Data<T>>(key: K): Data<T>[K] => data[key]
if (type === DataType.Bar) {
data; // still inferred as Data<T>
console.log(data.otherKey) // error Property 'otherKey' does not exist on type 'FooData | BarData'.
console.log(getter('otherKey')) // error Argument of type 'string' is not assignable to parameter of type 'keyof Data<T>'.
}
}
CodePudding user response:
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
}
}
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
}
}
getter
still does not work in a way you expect, hence I recomment to move it out from func