Skip to content
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

Suggestion: Type annotations and interfaces for function declarations #22063

Open
mbrowne opened this issue Feb 20, 2018 · 37 comments
Open

Suggestion: Type annotations and interfaces for function declarations #22063

mbrowne opened this issue Feb 20, 2018 · 37 comments
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@mbrowne
Copy link

mbrowne commented Feb 20, 2018

Currently in TypeScript, function declarations cannot be typed in the same way as function expressions, e.g. this function can implement the React.FC interface:

interface TestProps {
    message: string
}

const Test: React.FC<TestProps> = ({ message }) => {
    return <div>{message}</div>
}

But this function can't, at least not directly:

function Test({ message }: TestProps) {
    return <div>{message}</div>
}

This becomes more of an issue if you try to add properties to the function object:

Test.defaultProps = {
    message: 'hi'
}

It seems that currently the only way to specify the type for the function object in the second example is to create a new variable:

const AliasForTest: React.FC<TestProps> = Test
AliasForTest.defaultProps = {
    message: 'hi'
}

This seems like kind of an ugly workaround, so it seems that the current idiom is to just prefer arrow functions for cases like this. But this leads to inconsistency on teams that generally prefer function declarations over const expressions for top-level functions. Personally I find it more readable to see the word "function" for top-level functions rather than seeing "const", which is generally already all over the place in the code. There is even an ESLint rule for teams that share my preference (although I don't think it's been ported to TSLint yet): https://eslint.org/docs/rules/func-style. In any case, I have seen others express similar views and other codebases (including some from Facebook and Apollo, for example) that still prefer the "function" keyword for top-level functions.

However, stylistically it's also a problem if top-level functions are declared some places as declarations (using function) and in other places as expressions (using const). But for those who desire consistency, TypeScript is basically forcing the use of expressions, due to the issues described above.

This is far from being a top priority of course, but I was surprised to see that TypeScript didn't provide some equivalent typing syntax for function declarations. It would be great if this could be considered for a future version (even if far in the future). Thanks for reading!

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Feb 20, 2018
@michaeljota
Copy link

michaeljota commented Mar 8, 2018

I just was thinking about this. I would like to make a proposal to type functions declarations, by adding to the function keyword a generic param that can shape the function.

function<T extends (...V): R> // Maybe something like this

To be used like this:

function<ActionReducer<State>> reducer (state, action) {
  state // Type infers to State.
  action // Type infers to Action, as is its default value.
  return // Type infers to State.
}

Variadic types are currently being tracked at #5453, but maybe this isn't need for this to work, and this can be implemented somehow with the current union types, or the same way interfaces like this are being currently declared, but duplicating the amount of interfaces a having a any default to the nth interface.


Another proposal, should be supporting @type property of JSDocs. With this, we can be able to type functions.

/**
 * @type {ActionReducer<State, ActionType>}
 */
function reducer(state, action) {
  state // Type infers to State.
  action // Type infers to ActionType, as is its default value.
  return // Type infers to State.
}

@mbrowne
Copy link
Author

mbrowne commented Mar 14, 2019

Just curious if anyone on the TypeScript team is interested in working on this feature for some later release of 3.x? Where would this fit into the roadmap?

@rmolinamir
Copy link

rmolinamir commented Feb 9, 2020

Something like this would be amazing:

async function user: Resolvers.TQueryUser (_, { id }, context) {
  const user = await context.prisma.users.findOne({ where: { id: +id } });
  return user;  
}

My current alternative is to do:

const user: Resolvers.TQueryUser = async function user(_, { id }, context) {
  const user = await context.prisma.users.findOne({ where: { id: +id } });
  return user;  
}

But unfortunately, I wouldn't be able to type them, in say, within objects. E.g.:

{
    user: user: Resolvers.TQueryUser = async function user(_, { id }, context) {
      const user = await context.prisma.users.findOne({ where: { id: +id } });
      return user;  
    }
  }

Or:

{
    user: async function user: Resolvers.TQueryUser (_, { id }, context) {
      const user = await context.prisma.users.findOne({ where: { id: +id } });
      return user;  
    }
  }

I could also do:


interface IUserResolverMap {
  // Queries
  Query: {
    user: Beast.TQueryUser
    users: Beast.TQueryUsers
    me: Beast.TQueryUser
  }
  // Mutations
  Mutation: {
    newUser: Beast.TMutationNewUser
    authenticate: Beast.TQueryAuthenticate
  }
}

const resolvers: IUserResolverMap = {
  Query: {
    async user(_, { id }, context) {
      const user = await context.prisma.users.findOne({ where: { id: +id } });
      return user;  
    }
  }
  ...
}

But then I would get this complaint (some might not depending on your ESLint):

Missing return type on function.eslint(@typescript-eslint/explicit-function-return-type)

@dradetsky
Copy link

I realize this issue has been around for some time, but I just wanted to say: I still want this. When new to typescript, I had originally tried to write e.g.

type MyFn = (a: number, b: string) => number
declare function h: MyFn
function h(a, b) {
    return a
}

and was very disappointed when I found out you couldn't do this.

I just want to add that I personally would be happy with a much less slick syntax than the other commenters seem to be proposing. For example declare function f: T would be just fine. In addition to the other benefits proposed by the other commenters, this can be a great way to make your code easier to read since when I see

function g(a: number, b: string): number {
    return a
}

I'm trying to read the names of the parameters and follow then throught the function body, and the less busy the signature line is, the easier this is. For example, when I see

let e: (a: number, b: string) => number
e = (a, b) => {
    a
}

(NOTE: This is not what I actually want to read or write)

I can mentally separate the process of determining the types from following the variable names throught the function body. Although it's not obvious in this contrived example, I often read code (especially difficult-to-understand code) by going back and forth visually between the argument list and the function body to try to figure out what's going on. And being able to look up the types of the argument names separately from referring to the names themselves makes this easier.

@bodinsamuel
Copy link

+1 on this

This is really the most painful thing on Typescript right now (because everything else is quite perfect <3)
Especially when dealing with reusable express middleware, this is quickly getting out of hand, even a basic function declaration takes > 5 lines.
I would love some comments from maintainer (cc @RyanCavanaugh I know it's not on the roadmap for the moment)

// this simple js function
function myMiddleware(req, res, next);

// ... can become this monstrosity
function myMiddleware(
  req: express.request<
   MyParams, 
   MyBody
   MyQueries
  >, 
  res: express.response<MyBody>, 
  next: express.nextFunction
): Promise<void>;

Using const and anonymous function can "help" but you will reach 80colums quickly anyway, change the behavior (function and const has slight difference), and it still not as cool as having a proper way of doing it.

const middleware: Custom<MyParams, MyBody, MyQueries> = function(
  req, 
  res, 
  next
);

This could be something like this:

function: Custom<MyParams, MyBody, MyQueries> myMiddleware(req, res, next) ;

Note this is similar request as #39623

@ThaJay
Copy link

ThaJay commented Nov 5, 2020

I agree with this issue. Function declarations are not the same as function expressions and arrow functions are not the same as normal functions. I would like to keep using function as the default and only use () => when this is required from the definition context like with callbacks fron instance.

In my opinion it makes the code a lot easier and faster to read because function doSomething (param) {} looks like one token plus name when skimming over the code and const doSomething = param => {} looks like 2 tokens plus name.

current form in TypeScript React:

const ComponentName: React.FC<{}> = props => {}

// would be sort of equivalent to:
function ComponentName (props: PropsWithChildren<{}>): React.ReactNode {
  this.propTypes: WeakValidationMap<P> = null
  this.contextTypes: ValidationMap<any> = undefined
  this.defaultProps: Partial<P> = undefined
  this.displayName: string = ''
}

So it would be really nice if we could do:

function ComponentName: React.FC<{}> (props) {}

What's so special about function vs var, let or const?

@chharvey
Copy link

chharvey commented May 19, 2021

If the function type annotation appears before the generic parameters, how will it be able to reference those parameters?

Say we have a generic function type:

type ProcessingFunction<DocT extends Document, DataT> = (doc: DocT, data: DataT) => void;

and we want to apply it to an instance:

function process<DType>(doc: Document, data: DType): void {
	...
}

This instance can be called with different generics:

process<{url: string, id: number, text: string}>(document, {url: '//example.com', id: 42, text: 'lorem ipsum…'});
process<{title: string, items: string[]}>(document, {title: 'Hello World', items: ['one', 'two']});

How would the function be annotated?

function: ProcessingFunction<Document, DType> process<DType>(doc: Document, data: DType): void {
//                                     ^ Error: Cannot find name 'DType'
	...
}

Note that as pointed out by someone else on Twitter, this problem already exists in the form of variable declaration:

let process: ProcessingFunction<Document, DType> = function <DType>(doc: Document, data: DType): void {
//                                        ^ Error: Cannot find name 'DType'
	...
};

But I guess my question still stands of if/how the issue will be addressed. It seems to me that this feature — annotating function declarations — should be able to handle generics.

@mbrowne
Copy link
Author

mbrowne commented May 20, 2021

@chharvey The purpose of this proposal is to have syntax for function declarations (function ...) that's semantically equivalent to what you can do with function expressions (const myFunc ...). If you see issues with specifying types of function expressions, then I suggest you propose a solution for that first (or create a new issue about it), since that's the syntax that already exists in TypeScript. Then I'm sure some semantically equivalent syntax could be designed for function declarations.

If you already have an idea of what the syntax should look like for either declarations or expressions to support the use cases you mentioned, feel free to share so that those considering this proposal can be aware of it when designing the syntax.

@chharvey
Copy link

chharvey commented May 21, 2021

@mbrowne A few ideas:

  1. Type annotation after the function name and generic parameters (if any), followed by an equals sign:

    function process: ProcessingFunction = (doc: Document, data: unknown): void {
    	...
    }
    function process<DType>: ProcessingFunction<Document, DType> = (doc: Document, data: DType): void {
    	...
    }

    Con: A bit too much like variable declaration; leads people to believe they can remove the type annotations and be left with function process = (doc, data) {...}, which is invalid.

  2. Similar to 1 but with as instead of colon and no equals sign:

    function process as ProcessingFunction(doc: Document, data: unknown): void {
    	...
    }
    function process<DType> as ProcessingFunction<Document, DType>(doc: Document, data: DType): void {
    	...
    }

    Con: Type annotation too close to parameter list makes it look like function name / generic parameter list.

  3. Type annotation in colon delimiters:

    function process :ProcessingFunction: (doc: Document, data: unknown): void {
    	...
    }
    function process<DType> :ProcessingFunction<Document, DType>: (doc: Document, data: DType): void {
    	...
    }

    Con: Too unfamiliar; colon delimiters are found nowhere else in the language.

  4. implements after return type

    function process(doc: Document, data: unknown): void implements ProcessingFunction {
    	...
    }
    function process<DType>(doc: Document, data: DType): void implements ProcessingFunction<Document, DType> {
    	...
    }

    Con: void implements ProcessingFunction seems like a type per se.

  5. Type annotation after the entire function body (following colon or as).

    function process(doc: Document, data: unknown): void {
    	...
    }: ProcessingFunction;
    
    function process<DType>(doc: Document, data: DType): void {
    	...
    }: ProcessingFunction<Document, DType>;
    
    function process(doc: Document, data: unknown): void {
    	...
    } as ProcessingFunction;
    
    function process<DType>(doc: Document, data: DType): void {
    	...
    } as ProcessingFunction<Document, DType>;

    Con: Big functions would have the annotation too far down from the head.

@michaeljota
Copy link

I think this was one of the first issues I ever comment on Github, and we still don't have a way to correctly type function expressions. But now, I think I can see the problems this could bring. As I far as I understand TS, it resolves generics the same way JS resolves const variables, they need to be declared first before being used. So, how could you type something like:

interface FC<Params> {
  (params: Params): void;
}

interface FooParams<T> {
  bar: string;
  baz: number;
  data: T;
}
//                                  ▼ A    ▼ B   ▼ C
const FooBazComponent: FC<FooParams<T>> = <T>({ data }) => { 
  // ..
};

// Errors: 
// A: Error: Exported variable 'FooBazComponent' has or is using private name 'T'.(4025)
// B: 'T' is declared but its value is never read.(6133)
// C: Binding element 'data' implicitly has an 'any' type.(7031)

Playground


You see from the example that you can't, even without React, type a function declaration with a generic, because it will throw when it doesn't find the type name (Because they are scoped to the function where they are being declared).

Because of this, most of the proposal here are syntax won't work with generics, and I think that could be the reason behind why is this taking so long.

Consider the example I first use:

interface ActionReducer<Params> {
  (params: Params): void;
}

interface State<T> {
  bar: string;
  baz: number;
  data: T;
}

function<ActionReducer<State<T>>> reducer <T>({ data }, action) {
  state // Type infers to State.
  action // Type infers to Action, as is its default value.
  return // Type infers to State.
}

// Errors: 
// A: Error: Exported variable 'reducer' has or is using private name 'T'.(4025)
// B: 'T' is declared but its value is never read.(6133)
// C: Binding element 'data' implicitly has an 'any' type.(7031)

So, we would have the same problems.

However, @chharvey put some good examples when he puts the generics first. Even when all of them have some caveats when you want to actually use them, if we mix them, we may have something that could really be usable.

Keep in mind that TS type anotation should be easily removed, and should not produce invalid JS code when this is done. (This is a feature that Babel use, for example).

  • Type anotation with a semi colon

    function process: ProcessingFunction (doc: Document, data: unknown): void {
      ...
    }
    function process<DType>: ProcessingFunction<Document, DType> (doc: Document, data: DType): void {
      ...
    }

    Con: The generic position could be confusing, but this could be actually valid.

  • Interfaces implementation

    function process implements ProcessingFunction (doc, data): void {
      ...
    }
    
    function process<Data> implements ProcessingFunction<Data> (doc, data): void {
      ...
    }

    Pros: ES classes already have implements syntax in TS, so this could be convenient.
    Middle: implements could have a different meaning when using with classes than with functions. With classes, an interface should be consider a contract and every property in that interface should be declared by the class. However, if a function signature is declared using an Interface or a Type, and this function signature also have additional properties, what should TS do about this? Should throw and warn about the function not having such properties?

Of course, this is something that should be investigated, but I think that the interface implementation is the most proper syntax if we want to type function expressions, from the point of view of "What is tecnically doable" and "TS shouldn't transform JS code".

@max-hk
Copy link

max-hk commented May 31, 2021

How about using the way provided by #10421?

function Test({ message }: TestProps) {
    return <div>{message}</div>
}

assume Test is React.FC<TestProps>;

@michaeljota
Copy link

@MaxLOh probably a good workaround, but I don't think this would solve the generic issue. I think the generics issue is why this has taken so long to be even considered. It's not really as straightforward as you would think.

@mbrowne
Copy link
Author

mbrowne commented Jun 3, 2021

I don't think the generics issue has much to do with why this is taking a long time to be considered. I'm not on the TypesScript team, but I know there's a long backlog of issues and feature requests, and this request is essentially a secondary syntax for something that's already possible. Don't get me wrong, I'm the one who originally raised the issue and I'm in favor of adding this to the language, but adding better support for generics is a separate consideration. As someone already pointed out, this is a pre-existing issue with function expressions defined with const. At this point I think it would be best if a new issue were created about the generics issue.

@michaeljota
Copy link

this request is essentially a secondary syntax for something that's already possible.

Actually, for me, is just about doing something that I'm not able to do without it. You can't declare React components properly if those components use generics, as TS would take the generic declaration as a React element.

import { FC } from "react";

interface ComponentProps<Data> {
    data: Data;
}

const Component: FC<ComponentProps<Data>> = <Data>({ data }) => {
    return <>{JSON.stringify(data)}</>
}

Playground link

And if you use the function declaration, you are now with a function that can handle the generic and have the same shape of a React component, you are typing props, and the return, but you can't use other React features.

Playground link

This might be "workarounded" if the assume feature is implemented, with something like this:

function DeclarationComponent<Data>({ data }: ComponentProps<Data>): ReactElement | null {
    return <>{JSON.stringify(data)}</>; // var data: Data
}

assume DeclarationComponent is FC<ComponentProps>
// or even better, idk if this would work though.
assume DeclarationComponent<Data> is FC<ComponentProps<Data>>

Of course, you can reassign the function to a variable, or cast it, or something, but it would better if we have a syntax to support this.

@mbrowne
Copy link
Author

mbrowne commented Jun 12, 2021

@michaeljota I would still recommend opening a separate issue about generics and the new syntax you propose to add to the existing const/variable syntax (assuming you would eventually want this feature for functions declared either way and not only for function declarations). The points you're raising are unlikely to get any attention here in this thread for a long time. We're not even close to an official design for the original suggestion in this thread, let alone an implementation or adding additional support for generics.

@michaeljota
Copy link

michaeljota commented Jun 12, 2021

I mean, it's all about the same situation. There should not a special syntax for generics, there should be a syntax to type functions, and that syntax needs to support generics. Else, what we would do if we try to type a function with a type that requires generics arguments?

@michaeljota
Copy link

michaeljota commented Sep 3, 2021

@DanielRosenwasser I'm going to take my chances here, but the more I use TS and need to create a component with generics, the more I need this to be implemented. So, here is a polished version of my proposal:

implements keyword for functions to create a function contract interface.

implements keyword is currently used to create a class contract, extending the standard ES classes with OOP features. The implements keyword would be in the position as they are used in classes, after the generic declaration. Unlike classes, functions require parenthesis for arguments declaration. This declaration would start right after the implementation finished. The whole implements implementation could be removed in order to produce valid JS code, as it is done in ES classes. Supporting generics would be a required feature for this to work, and having the implements implementation after the function generic declaration allows the generic to be used in the scope of the implements. The type or interface implemented with implements can be anything. Call signatures in the given interfaces should be used to define the signatures of the function. If a class interface is used, the function should produce a function constructor.

To consider:

  • If a function implements multiple interfaces with multiple call signatures that can't be considerer an overload, what should the function typing be? Could it create an on-the-fly union typing? Should it throw with some error giving that they can't assign each other?

Some examples of this in pseudo-real life code:

React component example

interface TableProps<Data> {
  data: Data;
  columns: Column<Data>[];
  sortBy: { key: keyof Data }
}

function Table<Data> implements FC<TableProps<Data>> ({ data, columns, sortBy }) { // props are typed correctly and a valid React node return is required.
  ...
}

Table.defaultProps = {} // No errors, correctly shaped as Partial<TableProps<Data>>

Reducer example

interface State<Data> {
  data: Data;
}

import { Reducer, Action } from 'redux';

function reducer<Data> implements Reducer<State<Data>, Action> = (state, action) => { // state is typed State<Data>
  //
}

I think this could be the best way this could get implemented. Anything that I can do to help with this, I'll try to do it.

Update:

Please, consider this syntax can also be applied to anonymous functions and arrow functions as well:

React component example (anonymous function)

interface TableProps<Data> {
  data: Data;
  columns: Column<Data>[];
  sortBy: { key: keyof Data }
}

export function<Data> implements FC<TableProps<Data>> ({ data, columns, sortBy }) { // props are typed correctly and a valid React node return is required.
  ...
}

React component example (arrow function)

interface TableProps<Data> {
  data: Data;
  columns: Column<Data>[];
  sortBy: { key: keyof Data }
}

const Table = <Data> implements FC<TableProps<Data>> ({ data, columns, sortBy }) => { // props are typed correctly and a valid React node return is required.
  ...
}

Table.defaultProps = {} // No errors, correctly shaped as Partial<TableProps<Data>>

@penx
Copy link

penx commented Sep 17, 2021

Is it possible to use a defined type with generics on function expressions already? Or would this feature add this (or at least provide an alternative)?

I've posted a more detailed question here:

https://stackoverflow.com/questions/69226237/typescript-use-a-generic-type-for-a-function-declaration

@michaeljota
Copy link

@penx Thanks for asking that, and posting the link here. The answer they give inspire me to do this:

Component with Generics

export interface Component extends VFC<ComponentProps<{}>> { <Data>(props: ComponentProps<Data>): ReturnType<FC> };

export const Component: Component = ({ data, keys }: ComponentProps<Record<any, any>>) => {
    return <>{data.map(a => <h2>a</h2>)}</>
}

Declaring a type alias or an interface overload can help us to do the trick of having a component using generic types. :).

Of course, this still would be a better solution, as we wouldn't need to deal with overloads, but this workaround actually solves the issue without any tradeoff.

@ThaJay
Copy link

ThaJay commented Sep 28, 2021

this workaround actually solves the issue without any tradeoff.

The trade off is complexity / readability. Although Typescript projects often already have their fair share of arcane expressions. Luckily you can hide most of them away so junior devs can just use the types without thinking about them too much (if the code base is in an okay state)

@danielo515
Copy link

I realize this issue has been around for some time, but I just wanted to say: I still want this. When new to typescript, I had originally tried to write e.g.

type MyFn = (a: number, b: string) => number
declare function h: MyFn
function h(a, b) {
    return a
}

and was very disappointed when I found out you couldn't do this.

This is probably the proposal with better readability. Is this even being considered or is stale?

@jekh
Copy link

jekh commented Jan 13, 2022

I saw @michaeljota 's comment in #22063 (comment) and was inspired to try this out in a .js file. Typing function declarations already works with jsdoc!

// perhaps in a global.d.ts file
type NumberToStringFn = (a: number) => string


/** @type {NumberToStringFn} */
function numberToStringDeclaration(num) {
  return num.toString()
}

Typescript properly recognizes the type of numberToStringDeclaration:
image

If #42048 (jsdoc in .ts files) were implemented, this could potentially be a way to get typed function declarations without adding a new syntax.

@mbrowne
Copy link
Author

mbrowne commented Jan 13, 2022

@jekh That certainly sounds like a good feature, but the original reason I raised this issue was for consistency in syntax. The idea is to allow teams to decide their preferred coding style / linting preferences for functions without being forced to use const expressions instead of declarations in some instances just to be able to specify the types. Having jsdoc syntax as the only option for typing a function declaration would work against the goal of consistent syntax, except for teams that might prefer to use jsdoc syntax across the board for everything (not just functions).

So while I would be in favor of supporting jsdoc syntax, I hope the TypeScript team would still leave this issue open. But in the meantime, maybe jsdoc syntax couldl help address some of the issues with typing generic functions mentioned earlier in this thread.

@RebeccaStevens
Copy link

RebeccaStevens commented Jan 13, 2022

The JSDoc approach is definitely better than nothing but it wouldn't work with type declarations (i.e. cannot be used in .d.ts files).
For example we'd want to support something like this:

declare function foo: F;

Note: This would be different to declare const in the following case:

declare const foo: F;	// <-- Cannot redeclare block-scoped variable 'foo'. ts(2451)
declare namespace foo { /* ... */ }

@jekh
Copy link

jekh commented Jan 13, 2022

@mbrowne I fully share your desire to have first-class support in typescript for typing function declarations. In addition to stylistic and consistency benefits from it, it's currently simply not possible in typescript to get the benefits of function hoisting while also declaring the type of your function using an existing type.

The observation about jsdoc and function typing was more to point out that typescript is already capable of supporting this feature, at least internally, but limits it to .js files. It's also a possible work-around (if the referenced issue were implemented) while waiting for first-class typescript support.

I'd very much like to see this issue remain open and perhaps get a bit more discussion around what the syntax could/should be.

@michaeljota
Copy link

@jekh I'm glad you found my comment useful. I have to mention when I comment that I didn't consider function generics, that for me, it may be the only problem the TS team is having with this (I may be wrong, and probably I'm) That's because almost all proposals break when you need to declare a function with a generic. Here is a larger explanation and proposal #22063 (comment).

@a7madgamal
Copy link

a7madgamal commented Jun 7, 2022

not sure if this is related or not but please help if you can.
I'm creating an external type that I want to import and use as a method type.
Not just the input/output but completely override or overload this method because certain keys accept certain config values

Is this blocked by this issue or unrelated?

generated.ts

export type FuncType =
  | ((key: 'activityPlugin.activity', config?: Record<string, unknown>) => string)
  | ((key: 'activityPlugin.expenditureRate', config?: Record<string, unknown>) => string)
  | ((key: 'activityPlugin.manualEntry', config?: Record<string, unknown>) => string)
  .......

translations.ts

class Translations {
  // how can I tell ts to type this method as FuncType???
  t(key, config?) {
    return this.translate(key, config);
  }
}

@michaeljota
Copy link

michaeljota commented Jun 7, 2022

@a7madgamal It's unrelated, and I'm not sure if you can overload the string fallback. With module augmentation you could do something like:

declare class Translations {
  t(key; 'activityPlugin.activity', config?: Record<string, unknown>): string;
  t(key; 'activityPlugin.expenditureRate', config?: Record<string, unknown>): string;
}

Or better yet,

type ValidKeys = 'activityPlugin.activity' |  'activityPlugin.expenditureRate' | ...;

declare class Translations {
  t(key; ValidKeys, config?: Record<string, unknown>): string;
}

But this will only provide suggestions to the TS Server, I don't know if that would prevent you to input any string, because the default declaration uses a string.

@brokenthorn
Copy link

brokenthorn commented Oct 16, 2022

Wow, so many propositions, but I haven't seen this yet, which I think is more concise and aligns with other TypeScript idioms:

export default function Home() as NextPage {
    return (
      <Title>Home page</Title>
    );
}

NextPage is:

type NextPage<P = {}, IP = P> = React.ComponentType<P> & {
    getInitialProps?(context: NextPageContext): IP | Promise<IP>;
}

What the above allows is to preserve existing named/hoisted function declarations, with all their current functionality, by just adding an additional and optional type constraint at the end using the as keyword, which is used for type assertions. In this case with functions, I think it's an elegant solution as this is indeed an assertion over a clear type definition (the normal function type definition).

export default function Home(): ReactElement as NextPage {
    return (
      <Title>Home page</Title>
    );
}

The above would still be a valid declaration as Home's signature (void => ReactElement) is compatible with the signature of NextPage. We're just asserting that the function Home(): ReactElement should also satisfy the NextPage type's constraints as well.

That's my suggestion. Please forgive any mistakes, if I've made them, as I am a novice at writing TypeScript.

@tbremer
Copy link

tbremer commented Jul 12, 2023

Moving the content of my suggestion (#54989) to this one, seems like this would be an amazing feature that would align function declarations to function expressions in a meaningful way.

Example use case in Playground that echoes alot of what has already been mentioned. I also think this could do wonders for function overloads as well.

Example from docs:

/**
 * defined in `'some-types-from-somehwere'`
 * export type LenString = (s: string) => number;
 * export type LenArr = (arr: any[]) => number;
 */
import type { LenString, LenArr } from 'some-types-from-somehwere';

function<LenString | LenArr> (x) {
    return x.length;
}

@henrikvilhelmberglund
Copy link

This has been the most confusing thing for me when learning Typescript, I would like to keep using function a() for functions but Typescript basically forces me into using arrow functions if I want type annotations for the functions. Having this implemented so you can write the style you prefer would be great.

@ericmorand
Copy link

ericmorand commented Dec 1, 2023

@henrikvilhelmberglund it is not even a matter of preference: arrow function don't have access to this, so there is a fundamental difference betweenn the two kind of functions, and currently only one of them (arrow function) is considered as a first-class citizen, which is a bummer.

@ThaJay
Copy link

ThaJay commented Feb 15, 2024

@ericmorand That is so wrong. Either function should be a first class citizen as it has its own keyword and has way more features, or both.

@rwilliams3088
Copy link

If I might suggest a syntax for function type checking? I think this would work nicely:

type IMyFunc<P,R> = (params: P): R;

function MyFunc<P,R>(params: P) {
   ...
}: IMyFunc<P,R>;

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Apr 8, 2024

Due to how var and function declaration merging works in vanilla JS, you should technically be able to do the following:

// @showEmit
declare type IMyFunc = <P, R>(params: P) => R;

var MyFunc: IMyFunc;
function MyFunc(params) {
//       ^?
	// ...
}

Workbench Repro

@typescript-bot typescript-bot added the Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros label Apr 8, 2024
@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 8, 2024

👋 Hi, I'm the Repro bot. I can help narrow down and track compiler bugs across releases! This comment reflects the current state of this repro running against the nightly TypeScript.


Comment by @ExE-Boss

❌ Failed: -

  • Duplicate identifier 'MyFunc'.
  • Duplicate identifier 'MyFunc'.
  • Parameter 'params' implicitly has an 'any' type.

Emit:
"use strict";
var MyFunc;
function MyFunc(params) {
    // ...
}

Historical Information
Version Reproduction Outputs
5.0.2, 5.1.3, 5.2.2, 5.3.2, 5.4.2

❌ Failed: -

  • Duplicate identifier 'MyFunc'.
  • Duplicate identifier 'MyFunc'.
  • Parameter 'params' implicitly has an 'any' type.

Emit:
"use strict";
var MyFunc;
function MyFunc(params) {
    // ...
}

@rwilliams3088
Copy link

rwilliams3088 commented Apr 8, 2024

Due to how var and function declaration merging works in vanilla JS, you should technically be able to do the following:

// @showEmit
declare type IMyFunc = <P, R>(params: P) => R;

var MyFunc: IMyFunc;
function MyFunc(params) {
//       ^?
	// ...
}

Workbench Repro

That doesn't cover the same use-case as what I'd like to cover. In your example you are defining an interface that contains a function that is generic (and so the implementation must support any combination of P and R that the caller supplies). I don't think there are too many cases where that would really be desirable in and of itself.

What I'd like to see is a way to define a generic function type that can be applied to functions to validate that they not only satisfy the type constraints, but the generic type arguments can be refined for more specific use-cases. Much the same as we do with generic interfaces and classes.

For instance, I have a little plugin framework I've written on top of the ag-grid component so that you can have multiple things listen and respond to the GridReady event (as-is you can only pass in one event handler). I have a generic IPlugin<TData, TContext> interface and a PluginHook<TData, TContext> function type.

As I implement a hierarchy of plugins, some of which are generic and others are more specific to certain TData and/or TContexts, I would like Typescript to validate that my hook functions are properly satisfying the function type definition of a PluginHook, including any tighter type constraints placed on TData and TContext at that point in the plugin hierarchy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Has Repro This issue has compiler-backed repros: https://aka.ms/ts-repros In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests