How can I ensure that when I comment out line 37 with components: [1, 2]
, the argument c in available: (c) => {
is not never
but get type number
?
The main issue is that if I comment out this line, all the others entries, even if they have different components:[...]
, become (c:never)=>{}
.
I would also like the argument c
in method to fallback to type number
rather than never
if the components
property is not provided.
enum AAA {
a1 = 'a1', a2 = 'a2', a3 = 'a3',
}
enum BBB {
b1 = 'b1', b2 = 'b2', b3 = 'b3',
}
type TestData<T extends number = number> = {
components?: T[];
avaible: (c: T) => void;
}
// Adjust the RR type to allow partial records for both AAA and BBB
type RR = Partial<Record<AAA, Partial<Record<BBB, number>>>>;
type Param<T extends RR> = {
[KA in keyof T & AAA]?: {
[KB in keyof T[KA] & BBB]?: TestData<Extract<T[KA][KB], number>>
}
}
class Test<T extends RR> {
constructor(
public readonly data: Param<T>
) { }
}
// this is a db with literal entries
// i want pass to (c) type from .components or if not defined, juste use number
new Test(
{
[AAA.a1]: {
[BBB.b1]: {
components: [1, 2], // if i remove this, c should number and not never !?
avaible: (c) => {
// ^?
}
},
},
[AAA.a2]: {
[BBB.b2]: {
components: [5],
avaible: (c) => {
// ^?
}
},
[BBB.b1]: {
components: [8],
avaible: (c) => {
// ^?
}
},
},
}
);
It looks like the main problem is that when T
is constrained to RR
, then as soon as you comment out the line, the inference fails and then T
falls back to RR
, at which point you get Param<RR>
which is equivalent to {[K in AAA]?: {}}
and then everything stops working. I'm not 100% sure why that's happening, but type inference is tricky.
If you remove the constraint then things start to behave:
type Param<T> = {
[KA in keyof T]: {
[KB in keyof T[KA]]: (
TestData<Extract<T[KA][KB], number>>
)
}
}
class Test<T> {
constructor(
public readonly data: Param<T>
) { }
}
new Test(
{
[AAA.a1]: {
[BBB.b1]: {
avaible: (c) => {
// ^? (parameter) c: never
}
},
},
[AAA.a2]: {
[BBB.b2]: {
components: [5],
avaible: (c) => {
// ^? (parameter) c: 5
}
},
[BBB.b1]: {
components: [8],
avaible: (c) => {
// ^? (parameter) c: 8
}
},
},
}
);
Now only the missing components
has never
inferred. If you want this to fall back to number
instead (which shouldn't be necessary, you are always allowed to annotate (c: number) => {}
if you want), then you can use an additional conditional type:
type OrElse<T, D> = [T] extends [never] ? D : T;
type Param<T> = {
[KA in keyof T]: {
[KB in keyof T[KA]]: (
TestData<OrElse<Extract<T[KA][KB], number>, number>>
)
}
}
new Test(
{
[AAA.a1]: {
[BBB.b1]: {
avaible: (c) => {
// ^? (parameter) c: number
}
},
},
[AAA.a2]: {
[BBB.b2]: {
components: [5],
avaible: (c) => {
// ^? (parameter) c: 5
}
},
[BBB.b1]: {
components: [8],
avaible: (c) => {
// ^? (parameter) c: 8
}
},
},
}
);
Looks good!
At this point the only issue is that T
can have all kinds of keys, not just AAA
or BBB
:
new Test({x: {}}); // no error?!
You can add back the constraint in a number of ways, possibly by mapping properties with unexpected keys to never
, like:
type Param<T> = {
[KA in keyof T]: {
[KB in keyof T[KA]]: (
TestData<OrElse<Extract<T[KA][KB], number>, number>>
) & (KB extends BBB ? unknown : never)
} & (KA extends AAA ? unknown : never)
}
which leaves the good cases alone but gives you errors on
new Test({ x: {} }); // error!
I'll say the exact way to prevent unexpected key/values is out of scope here, and that the main point is to remove the constraint extends RR
and to use a conditional type on Extract<T[KA][KB], number>
to cause never
to become number
.