petewritescode
petewritescode

Reputation: 23

TypeScript functional destructuring not enforcing default type

I have a function which accepts a single object parameter as follows. (Note, this is a contrived example but it demonstrates the problem.)

type FuncParams = { sayYes: boolean; }
type FuncType = (params: FuncParams) => string;

const maybeSayYes: FuncType = ({ sayYes }) => sayYes ? 'Yes' : 'No';

maybeSayYes({ sayYes: true }); // Returns 'Yes'
maybeSayYes({ sayYes: false }); // Returns 'No'

I want to make the sayYes property optional, so I updated the types as follows and added a default to the function declaration:

type FuncParams = { sayYes?: boolean; }
type FuncType = (params: FuncParams) => string;

const maybeSayYes: FuncType = ({ sayYes = true }) => sayYes ? 'Yes' : 'No';

maybeSayYes({ sayYes: true }); // Returns 'Yes'
maybeSayYes({ sayYes: false }); // Returns 'No'
maybeSayYes({}); // Returns 'Yes'

However, I noticed that I can assign any default value to sayYes and TypeScript doesn't flag up the incorrect type. For example:

const maybeSayYes: FuncType = ({ sayYes = 'notABoolean' }) => sayYes ? 'Yes' : 'No';

I expected TypeScript to infer the boolean type from FuncType and stop me assigning a string, but it doesn't.

I can get the error to show if I explicitly type the parameter as follows:

const maybeSayYes: FuncType = ({ sayYes = 'notABoolean' }: FuncParams) => sayYes ? 'Yes' : 'No';

However, I assumed TypeScript would infer this type from FuncType. I'm sure it's my understanding of TypeScript that's at fault, but I'd like to know:

  1. Why TypeScript doesn't tell me that 'notABoolean' isn't a boolean.
  2. How I can constrain the default value type without having to duplicate the FuncParams typing.

Reproduction on TS Playground

Upvotes: 2

Views: 101

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074335

I'm surprised by the behavior you describe, but my knowledge of TypeScript is still not very deep.

You can do it using a function overload type:

type FuncParams = { sayYes?: boolean; }
type FuncType = {
    (params: FuncParams): string;
    (params: FuncParams & {sayYes: never}): string; // Or just: {params: {sayYes: never}: any;
};

Then, a call that tries to use sayYes with a non-boolean type will match the second overload and cause an error. (Many thanks to jcalz for pointing out that sayYes can remain optional, and that you can use a simpler signature for he never overload.)

That gives you this behavior:

// Correct default value type
const maybeSayYes1: FuncType = ({ sayYes = true }) => sayYes ? 'Yes' : 'No';

// Incorrect default value type
const maybeSayYes2: FuncType = ({ sayYes = 'notABoolean' }) => sayYes ? 'Yes' : 'No';
//    ^−−−−−−−−−−−−− TypeScript compiler error here

// Correctly don't have it at all (since it's optional)
const maybeSayYes3: FuncType = ({ }) => Math.random() ? 'Yes' : 'No';

Playground link

Upvotes: 3

Related Questions