6

edited: fixed "test" to "constraint" in interfaces

I am having trouble understanding how generic constraints are working in Typescript. I am working with React/Redux where you can find construction similar to this one:

interface SomeConstraint { constraint: any }
type SomeType<T> = <A extends SomeConstraint>(item: T, action: A) => void;

As you can see, this is a function definition with generic parameters and some constraint. Now, when I want to use it, I can write the following:

interface Test { constraint: string }
const some : SomeType<string> = (s: string, c: Test ) => {}

Which works great, but then, when I want to extend the test interface (let's call it TestWithData):

interface TestWithData  { constraint: string, payload: any }
const some : SomeType<string> = (s: string, c: TestWithData ) => {}

I get a compile time error:

error TS2322: Type '(s: string, c: TestWithData) => void' is not assignable to type 'SomeType'. Types of parameters 'c' and 'action' are incompatible. Type 'A' is not assignable to type 'TestWithData'. Type 'SomeConstraint' is not assignable to type 'TestWithData'. Property 'test' is missing in type 'SomeConstraint'.

What I am missing?


Edited

As I said I found this (similar) construct in redux typings. You can find this definition (redux "3.7.2"):

export type Reducer<S> = <A extends Action>(state: S, action: A) => S;

Now when you want to define your reducer you need to provide state and action, I'll skip the state part, so, I can write action to something like this:

interface MyAction { type: "MY_ACTION" }

and creating reducer is easy:

const reducer: Reducer<MyState> = (state: MyState, action: MyAction) => {...}

which will pass compilation, but if I add additional data to MyAction as following:

interface MyAction { 
    type: "MY_ACTION";
    payload: any
}

compilation will fail with the mentioned error. I was suspecting that it would pass. So what is the purpose for this constraint? To tell compiler that we expect exactly the same type (structurally) and nothing more or less? I think that this is a very limited use case, I would expect that the type inference would pick the type, check it's structural compatibility and preserve the type signature. Something like this but without need to specify the type argument:

export type Reducer<S, A extends Action> = (state: S, action: A) => S;

Now, type signature is preserved, but we need to specify the actual parameter type when declaring a variable (same as we do for state):

const reducer: Reducer<MyState, MyAction> = (state: MyState, action: MyAction) => {...}

1 Answer 1

5

For the example you have given, I don't think generic + constraint is the way I would define the type (it probably would be how I would do it in a nominal type system, but not in a structural type system).

type SomeType<T> = <A extends SomeConstraint>(item: T, action: A) => void;

If you are saying "the item must have a constraint property, you can get this using the simpler:

type SomeType<T> = (item: T, action: SomeConstraint) => void;

Structural typing takes care of the rest.

Here is my updated version as the previous answer stopped working in TypeScript 3.8.3. I have made the constraint generic as the constraint will be the same type as the item. Because the parameter types can be inferred, we don't need to state them explicitly (i.e. we don't need to write string three times... like this const actualImplementation: SomeType<string> = (item: string, action: string): void => { } - we can just write it once as the type argument.

interface SomeConstraint<T> {
    constraint: T
}

type SomeType<T> = (item: T, action: SomeConstraint<T>) => void;

const actualImplementation: SomeType<string> = (item, action): void => { }

const a: SomeConstraint<string> = { constraint: '' };

actualImplementation('', a);

The key point in the above example is that the actual implementation can be your Test type with no errors.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks fenton, I understand what you are saying, but unfortunately my example was wrong, I have accidentally left name of the property "test" in both interfaces, but it should be named "constraint". As for why this is defined this way it is out of my scope, this is actually ts typing for redux: export type Reducer<S> = <A extends Action>(state: S, action: A) => S;
But aside from redux, shouldn't this construct be the same as: type SomeType<T, A extends SomeConstraint> = (item: T, action: A) => void?
This answer does not seem to compile in typescript 3.7.5 and 3.8.3
Great answer! My question though is why does extending not allow additional properties. What causes it to break? It's still structurally typed right?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.