Skip to content

Explicit generic type argument gets lost when attempting to carry through member expression #32937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jeffijoe opened this issue Aug 16, 2019 · 7 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@jeffijoe
Copy link

TypeScript Version: 3.5.3

Search Terms: inference nested member

Code

The Playground link reproduces this issue, but here is the whole code.

// Issue: the generics are not flowing all the way through.

// This works fine
const handler1 = CommandHandler.create<RespondToQuestion>(
    requireThat(
        isCollaboratorOnRequest()
    ),
    requireThat(
        subjectIs(allowedTo.fooTheBar, BasedOnClaims.fromMessageBody(b => b))
    ),
    async ({ body }) => {
        console.log(body.subject_id)
        return { version: 1 }
    }
)

// But when I start chaining the specifications, the type that has been specified to be `RespondToQuestion` is
// now `unknown`.
const handler2 = CommandHandler.create<RespondToQuestion>(
    requireThat(
        isCollaboratorOnRequest().or(
            subjectIs(allowedTo.fooTheBar, BasedOnClaims.fromMessageBody(b => b))
        ) 
    ),
    async ({ body }) => {
        console.log(body.subject_id)
        return { version: 1 }
    }
)

// Specifying the generic seems to do the trick though, but why do I have to? Shouldn't it flow through?
const handler3 = CommandHandler.create<RespondToQuestion>(
    requireThat(
        isCollaboratorOnRequest<RespondToQuestion>().or(
            subjectIs(allowedTo.fooTheBar, BasedOnClaims.fromMessageBody(b => b))
        ) 
    ),
    async ({ body }) => {
        console.log(body.subject_id)
        return { version: 1 }
    }
)


const msg: Message<RespondToQuestion> = { body: { type: 'RespondToQuestion', subject_id: '123' }, meta: null as any }
handler1(msg)
handler2(msg)

handler3(msg)



// Sorry for all the types 😅
export interface AsyncSpecification<T> {
  and(other: AsyncSpecification<T>): AsyncSpecification<T>;
  or(other: AsyncSpecification<T>): AsyncSpecification<T>;
  execute(value: T): Promise<T>;
}

export declare function isCollaboratorOnRequest<T>(): AsyncSpecification<Message<T>>;

export interface Message<T, M = unknown> {
  body: T;
  meta: M;
}

export interface Claims {
  subject_id: string;
}

export type ClaimsLookupStrategy<T> = (message: Message<T>) => Promise<Claims>;

export type AllowedToBasedOnClaims = (claims: Claims) => Promise<Claims>;

export declare function subjectIs<T>(
  checkAllowedToBasedOnClaims: AllowedToBasedOnClaims,
  claimsLookupStrategy: ClaimsLookupStrategy<T>
): AsyncSpecification<Message<T>>;

export interface AllowedTo {
    fooTheBar: AllowedToBasedOnClaims;
}

export declare type Handler<I, O = unknown> = (input: Message<I>) => Promise<O>;
export declare type Pipe<I, O = unknown> = (input: Message<I>, next: Handler<I, O>) => Promise<O>;

export interface CommandResult {
    version: number | null;
}

export interface Command { type: string }

export declare const CommandHandler: {
    create: <C extends Command>(...pipes: Pipe<C, CommandResult>[]) => Handler<C, CommandResult>;
};

export declare function requireThat<I, O>(specification: AsyncSpecification<Message<I>>): Pipe<I, O>;

export interface RespondToQuestion extends Command {
    type: 'RespondToQuestion'
    subject_id: string
}

export declare const allowedTo: AllowedTo

export declare type ClaimsWithOptionalSubjectId = Omit<Claims, 'subject_id'> & {
    subject_id?: string;
};
export declare const BasedOnClaims: {
    fromMessage<T>(messageBodyToClaims: (message: Message<T, unknown>) => ClaimsWithOptionalSubjectId): ClaimsLookupStrategy<T>;
    fromMessageBody<T>(messageBodyToClaims: (body: T) => ClaimsWithOptionalSubjectId): ClaimsLookupStrategy<T>;
};

Expected behavior:

The type argument RespondToQuestion passed to CommandHandler.create<RespondToQuestion>() should flow all the way through consistently.

Actual behavior:

When using certain patterns, TS will change the type to unknown.

Additionally, it seems it can't make up its' mind. 😅 The error says body can't be inferred, and yet the tip gets it right.

image


image

Playground Link: Playground

Related Issues: These look related: #30134, #16597

@fatcerberus
Copy link

One thing that might prevent type parameters from flowing through is this:
https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-type-inference-work-on-this-interface-interface-foot---

I don’t know if that’s your issue, but it’s the first thing that came to mind.

@jeffijoe
Copy link
Author

@fatcerberus checked it out, if that was the case then my first example wouldn't work either, no?

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Aug 16, 2019

isCollaboratorOnRequest<T>()

This piece of code is way too suspicious.


It makes sense it infers unknown when all you do is call isCollaboratorOnRequest().

There is no constraint so the constraint on T is implicitly unknown.

It cannot infer T from any parameters because there are no parameters. So, it infers the constraint type. Which is unknown


In general, type inference flows from inner to outer. Constraints and whatnot change the rules a little.

But, fundamentally, you're expecting behavior that is completely different from what TS has.


TS doesn't say,

Oh, I can't infer T because there are no parameters, let me look at the outer function call and try to match the constraint there


I'm on mobile so I'll take a closer look when I get home but those are my current thoughts

@jeffijoe
Copy link
Author

@AnyhowStep it works fine on its' own without the chaining; and the T should be inferred from the return type?

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Aug 16, 2019
@RyanCavanaugh
Copy link
Member

This needs a reasonable-sized repro before we can look at it - once things get this long, 99 times out of 100 it is a problem with the code itself, not TS.

@jeffijoe
Copy link
Author

@RyanCavanaugh fair, but why can't TS make up its' mind about what type body is in the screenshots I posted? One part says its implicitly any, another is perfectly aware of the correct type. 🤔

@AnyhowStep
Copy link
Contributor

Your handler1 working surprises me, honestly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

4 participants