Reputation: 14750
I have a typescript function that accepts two params, a config object
and a function
:
function executeMaybe<Input, Output> (
config: { percent: number },
fn: (i: Input) => Output
): (i: Input) => Output | 'maybe next time π' {
return function(input: Input) {
return Math.random() < config.percent / 100
? fn(input)
: 'maybe next time π';
};
}
Usage of the function looks like this:
interface Foo { number: number; }
interface Bar { string: string; }
const makeBarMaybe = executeMaybe<Foo, Bar>(
{ percent: 75 },
foo => ({ string: `derived from Foo: ${JSON.stringify(foo)}` })
);
If I attempt to add a nonexistent property to my config object literal, TypeScript kindly informs me:
Argument of type '{ percent: number; baz: string; }' is not assignable to parameter of type '{ percent: number; }'. Object literal may only specify known properties, and 'baz' does not exist in type '{ percent: number; }'. (2345)
However, when I add an additional property to the object returned from the function param, no error is mentioned:
Is it possible to have TS provide an error in this scenario without explicitly providing the function's return type?
Here's a StackBlitz where I was playing around with this.
Upvotes: 1
Views: 432
Reputation: 33091
You see this error because of excess-property-checks. Literal objects are treated in a differen way.
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the βtarget typeβ doesnβt have, youβll get an error:
If you still want to add more extra properties to your argument, you should expect a subtype of Foo
instead of super type. I mean, that your argument should extend
Foo
instead of being Foo
.
You need to rewrite your executeMaybe
and apply a constraint where callback
function expects subtype of Input
instead of Input
itself:
function executeMaybe<Input, Output>(
config: { percent: number },
fn: <Inp extends Input>(i: Inp) => Output
): <Inp extends Input>(i: Inp) => Output | 'maybe next time π' {
return function (input: Input) {
const result =
Math.random() < config.percent / 100 ? fn(input) : 'maybe next time π';
console.log(result);
return result;
};
}
WHole code:
function executeMaybe<Input, Output>(
config: { percent: number },
fn: <Inp extends Input>(i: Inp) => Output
): <Inp extends Input>(i: Inp) => Output | 'maybe next time π' {
return function (input: Input) {
const result =
Math.random() < config.percent / 100 ? fn(input) : 'maybe next time π';
console.log(result);
return result;
};
}
console.clear();
interface Foo {
number: number;
}
interface Bar {
string: string;
}
const makeBarMaybe = executeMaybe<Foo, Bar>({ percent: 75 }, (foo) => ({
string: `derived from Foo: ${JSON.stringify(foo)}`,
}));
const explicitFoo: Foo = { number: 10 };
const implicitFoo = { number: 20 };
const superFoo = { number: 30, extra: 'super' };
makeBarMaybe(explicitFoo);
makeBarMaybe(implicitFoo);
makeBarMaybe(superFoo); // same object | duck typing allows
makeBarMaybe({ number: 30, extra: 'super' }); // ok
UPDATE
My bad, probably did not understand the question.
TypeScript does not support exact types, but there is a workaround:
type PseudoExact<T, U> = T extends U ? U extends T ? U : never : never
function executeMaybe<Input, Output>(
config: { percent: number },
fn: <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output
): <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output | 'maybe next time π' {
return function <Inp extends Input>(input: PseudoExact<Input, Inp>) {
const result =
Math.random() < config.percent / 100 ? fn(input) : 'maybe next time π';
console.log(result);
return result;
};
}
PseudoExact
- checks whether supertype extends subtype.
WHole code with expected errors:
type PseudoExact<T, U> = T extends U ? U extends T ? U : never : never
function executeMaybe<Input, Output>(
config: { percent: number },
fn: <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output
): <Inp extends Input>(i: PseudoExact<Input, Inp>) => Output | 'maybe next time π' {
return function <Inp extends Input>(input: PseudoExact<Input, Inp>) {
const result =
Math.random() < config.percent / 100 ? fn(input) : 'maybe next time π';
console.log(result);
return result;
};
}
console.clear();
interface Foo {
number: number;
}
interface Bar {
string: string;
}
const makeBarMaybe = executeMaybe<Foo, Bar>({ percent: 75 }, (foo) => ({
string: `derived from Foo: ${JSON.stringify(foo)}`,
}));
const explicitFoo: Foo = { number: 10 };
const implicitFoo = { number: 20 };
const superFoo = { number: 30, extra: 'super' };
makeBarMaybe(explicitFoo);
makeBarMaybe(implicitFoo);
makeBarMaybe(superFoo); // expected error
makeBarMaybe({ number: 30, extra: 'super' }); // expected error
Instead of my naive PseudoExact
you can use more advanced type utility. Please see this comment
export type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
Upvotes: 1