I am writing a callback function type.
One parameter called A is boolean , another parameter called B depends on this boolean parameter:
if A is true , B will be an object
if A is false , B will be undefined
like this
login({callback : (success , res) => {
if(success === true){
console.log(res.a) //ok
} else{
console.log(res.a) // error: res is undefined
}
}})
I wrote a type but it does not work
interface Options {
callback?: <T extends boolean>(success: T ,res: T extends true? {a : string}:never) => void;
}
login({callback : (success , res) => {
if(success === true){
console.log(res.a) //ok
} else{
console.log(res.a) // also ok
}
}})
CodePudding user response:
What you want to do is not possible. When you type a function with generic types, you type the parameters and the return values. The generic types are resolved/inferred in the code where the function is being called and it verifies the consistency of the parameters and return value types.
Inside the implementation, the types do not resolve so they remain T
and T extends true? {a : string } : never
.
The best thing you could do is use type checks outside callback
's implementation, and use function overloading instead of conditional type, in order to make the second parameter optional:
function login({ callback }: { callback: Callback }) {
const success = !Math.floor(Math.random() * 2);
if (success) {
callback(success, { a: 'test' }); // OK
callback(success); // Fails
}
else {
callback(success, { a: 'test' }); // Fails
callback(success); // OK
}
}
interface Callback {
(success: true, res: { a: string }): void;
(success: false): void;
}
CodePudding user response:
From the point of view of the implementation of login()
, it would be best for the callback
property of the function parameter to accept arguments which are either of type [true, {a: string}]
or of type [false, undefined?]
. Traditionally you would write this as an overloaded function with multiple call signatures:
function login({ callback }: {
callback: {
(success: true, res: { a: string }): void;
(success: false, res?: undefined): void;
}
}) {
callback(true, { a: "hey" });
callback(false);
}
You will get a little more type safety if instead you represent the function as taking a rest parameter of a tuple type, or rather, a union of tuple types:
function login({ callback }: {
callback: (...args:
[success: true, res: { a: string }] |
[success: false, res?: undefined]
) => void
}) {
callback(true, { a: "hey" });
callback(false);
}
When calling callback(true, {a: "hey"})
or callback(false)
, either version work, but you can't really get type safety when implementing the callback itself with overloads. For the rest of this answer I will assume you're using the union-of-tuple-rest-parameter approach.
The union [success: true, res: {a: string}] | [success: false, res?: undefined]
is a discriminated union, where the first element (with an index of 0
) is the discriminant property. If you have a value args
, you can check args[0]
to determine whether or not the second element (with an index of 1
) is a {a: string}
or undefined
.
So when calling login()
, you can get type safety if you use args
as a rest parameter and check its 0
and 1
indices:
login({
callback: (...args) => {
if (args[0]) {
console.log(args[1].a) // okay
} else {
console.log(args[1].a) // error: undefined
}
}
})
But you can't get the same safety with an arrow function that takes two separate parameters instead of a rest parameter:
login({
callback: (success, res?) => {
if (success) {
console.log(res.a) // error!
} else {
console.log(res.a) // error
}
}
})
The compiler does not track the correlation between success
and res
. This is a current limitation in TypeScript. It still doesn't track the correlation if you use destructuring assignment:
login({
callback: (...[success, res]) => {
if (success) {
console.log(res.a) // error!
} else {
console.log(res.a) // error
}
}
})
Both of these run into the same problem where assigning the pieces of the union to success
and res
variables break the correlation. TypeScript 4.4 added a feature, as implemented in microsoft/TypeScript#44730, which helps a little, as it lets you use success
to narrow args
, but it still doesn't let you use success
to narrow res
:
login({
callback: (...args) => {
const [success, res] = args;
if (success) {
console.log(res.a) // error
console.log(args[1].a) //ok
}
}
})
In that pull request it says:
[T]he pattern of destructuring a discriminant property and a payload property into two local variables and expecting a coupling between the two is not supported as the control flow analyzer doesn't "see" the connection. ... We may be able to support that pattern later, but likely not in this PR.
So there's some hope that in a future version of TypeScript you can write (success, res?) => ...
and things will magically work. For now, though, you have to refactor your code a bit.
And this, of course, leaves aside a more simple refactoring: just forget success
entirely and use res
instead:
function fixLogin(cb: (res?: { a: string } | undefined) => void) {
login({ callback: (success, res?) => cb(res) });
}
fixLogin((res?) => {
if (res) {
console.log(res.a) // okay
}
})
After all, success
doesn't give you any additional information over res
. If res
is present and defined, then success
had to be true
, and if res
is undefined
, then success
had to be false
. For all I know your code is a toy example and a similar refactoring isn't possible, but I would be remiss in not pointing this out, since it considerably simplifies both the call to and implementation of login()
.