microsoft/TypeScript

Type annotations for default export

mohsen1 opened this issue Β· 53 comments

TypeScript Version: 2.1.1

Code

import * as webpack from 'webpack';
export default: webpack.Configuration {
};

Expected behavior:
No error

Actual behavior:

[ts] Expression expected. error at default:

I couldn't find an issue for this but it's very likely it's a duplicate


Please πŸ‘ on this issue if you want to see this feature in TypeScript and avoid adding "me too" comments. Thank you!

Would not this be sufficient?

const config: webpack.Configuration  = { 

}
export default config;

Yes, that's what I do know but I wish I didn't have to.

related to #3792. we have tried to keep the module export as simple as possible on the syntax side.

I think it's a little more related to to the following scenario regarding function declarations.

if I want to write a decorator that is verified against the PropertyDecorator type in lib.d.ts, I can't write it easily. I have to use a function expression.

export let Encrypt: PropertyDecorator = function (obj, propName) {
};

which is case of friction whenever I want to actually implement a decorator without making a mistake.

I am trying to use a default export in the following way

readdirSync(join(__dirname, "../controllers/box"))

  .filter(f => f !== "*.spec.ts")
  .forEach(controllerFile => {
    const controllerBaseName = basename(controllerFile, ".js")
    import(`../controllers/box/${controllerBaseName}`).then((controller)=>{
      appRouter.use(
        `/box/${controllerBaseName}`, controller.default(boxServiceAccountClient))
    }).catch(error=>{
      console.log(error)
    })
  }); //end forEach

I get an error:

TypeError: controller.default is not a function at fs_1.readdirSync.filter.forEach.Promise.resolve.then.then.controller (/Users/bbendavi/Documents/cdt-box/dist/server/config/routes.js:16:71) at <anonymous> at process._tickCallback (internal/process/next_tick.js:160:7) at Function.Module.runMain (module.js:703:11) at startup (bootstrap_node.js:193:16) at bootstrap_node.js:617:3

I asked this on SO with a full file examples:
https://stackoverflow.com/questions/48696327/how-to-define-a-module-default-export-type-in-typescript

Would much appreciate your help!

import * as webpack from 'webpack'

export default {
...
} as webpack.Configuration

@IgorGee That's the proper way of doing it IMHO

The problem with

export default ... as X

is that it's a cast, so it purposely loses type safety.

@pelotom As a general matter of fact, you're completely right, I'd avoid putting this in my codebase. For the scope of this thread where this is about webpack configuration, I think we're just fine

@jlouazel leaving aside whether type safety is any less important in a webpack config... the issue is not specific to webpack, it's about annotating the type of the default export in general.

@IgorGee That's forbidden in @typescript-eslint/recommended.

const as = <T>(value: T) => value

export default as<webpack.Configuration>({
    // ...
})

Would love to see this feature for nice succinct code

If you're exporting a function, put it in parenthesis before the as.
e.g.

export default ((req, res) => {
   // Intellisense Enabled on `req` & `res`!
   return 'Hello World!';
}) as RequestHandler;

🚩Edit for downvoters: Typescript does check functions for return type & parameter compatibility when typecasting. Unlike typecasting for object types, functions retain a degree of type safety.

I have the same problem too. Need to type default export instead of cast.

could also use an iife so that the type is at the beginning of the export rather than the end

export default ((): MyType => ({
  k: v
})();
ackvf commented

While this gives me type hints inside the function (thanks @mccallofthewild )

export default (({ withIcon, children }) => {
  return <SomeJSX withIcon={withIcon}>{children}</SomeJSX>
}) as React.FC<{withIcon: boolean}>

I would still prefer to have the type declared up front (not sure about the syntax here though)

export default: React.FC<{withIcon: boolean}> (({ withIcon, children }) => {
  return <SomeJSX withIcon={withIcon}>{children}</SomeJSX>
})

Maybe export const default :Type = value; export type default = Type; export interface default {} could bring us more uniformity, avoid to introduce a new set of grammars just for default?

The solution proposed by @IgorGee works but is not fully reliable for type checking.

My default export is a ResourceProps where the property name is required.

If I try with a typed constant (the actual only solution):

const resourceProps: ResourceProps = {};
export default resourceProps;

I will have:

Property 'name' is missing in type '{}' but required in type 'ResourceProps'.  TS2741

But nothing if I do it like this:

export default {} as ResourceProps;

This make things harder to debug if something goes wrong because of this missing property. And we are using Typescript for that, right? :-)

However, I also understand the need to type the default export. On some cases, we just return a configuration object like that:

const resourceProps: ResourceProps = {
  name:"users",
  icon: UserIcon,
  list: UserList,
  create: UserCreate,
  edit: UserEdit,
  options:{
    label: 'Utilisateurs',
  },
};

export default resourceProps;

It's a bit cumbersome to be forced to declare a new variable just to take benefit of typing.

That was my two cents. I wanted to illustrate with some samples because I was myself a bit confused as a TypeScript beginner when I saw the lot of down votes on the previous solutions.

what about: export default <MyType>{ prop: 'value' }; ?

Edited: no a good solution, it's a reverse assertion, it's even worst than as MyType.

what about:

export default <MyType>{
  prop: 'value',
};

?

Great way, works for me, save me some typing

what about:

export default <MyType>{
  prop: 'value',
};

?

interface MyType {
  prop: string;
  prop2: string;
}

const toExport: MyType = {
  prop: "value",
}

export default <MyType>{
  prop: "value",
};

in this example TS fails on toExport variable (Property 'prop2' is missing...) and passes on export default <MyType>{...}, but should fail too

what about:

export default <MyType>{
  prop: 'value',
};

?

It is not a type declaration but a type assertion.
In other words, literally the same as as MyType, just another syntax. It was already discussed and discouraged here.

@richard-ejem I fully disagree, it's not a type assertion:

interface MyType {
  hello: string;
}

export default <MyType>{ hello: false }; // πŸ‘‰ Type Error

export default <MyType>{ hello: 'world!' }; // πŸ‘‰ Type Checked

Unless… you're right… it's a reversed assertion. It's even worst because in the case of missing props, there is no error whatsoever, but with as MyType there is still a type error.

@zaverden that's indeed strange… It looks like an in-between check

@richard-ejem I fully disagree, it's not a type assertion:

interface MyType {
  hello: string;
}

export default <MyType>{ hello: false }; // πŸ‘‰ Type Error

export default <MyType>{ hello: 'world!' }; // πŸ‘‰ Type Checked

Yes, it is an assertion, replace it with as and you get the same result.

interface MyType {
  hello: string;
}

export default { hello: false } as MyType; // πŸ‘‰ Type Error

export default { hello: 'world!' } as MyType; // πŸ‘‰ Type Checked

The type error is there because assertions between completely incompatible types are illegal, please read the TS error message you get.

see https://www.tutorialsteacher.com/typescript/type-assertion , <TYPE> is old syntax for assertions, now rarely used because its syntax conflicts with JSX.

@richard-ejem Yes, I edited my initial comment to reflect that it's not a valid notation πŸ˜‰ Thanks.

I think using semantics export default: Type Expression might add some restrictions, for instance, array definition with spaces between type and []. Using spaces between type and [] is allowed to define array type

const a: number         [];

How this case should be handled in export default? Should spaces between type and [] be forbidden? For the following example I expect an error that array is not assignable to number;

export default: number [];

It seems that the main concern with using the as operator is that it simply casts to a type without checking for type compatibility.

There is a draft proposal for adding the new satisfies operator that might fix this issue - #46827.

It allows checking type like so

interface Foo {
  a: number;
}
export default {} satisfies Foo; 
                            ^^^
Type '{}' does not satisfy the expected type 'Foo'.
  Property 'a' is missing in type '{}' but required in type 'Foo'.(1360)

Example

@RyanCavanaugh @DanielRosenwasser What do you think? Can we add this case to #46827 proposal use cases?

@a-tarasyuk I don't think syntax is ambiguous.

export default: number[];
// same as 
type Default = number[];
export default Default;
export default: number[] = [];
// same as 
const Default: number[] = [];
export default Default;

I am confusing about this too. any suggestion about typing the default export function?

I am confusing about this too. any suggestion about typing the default export function?

Currently you have to put it into a const first, then export it:

const dftExport: SomeType = { ... };
export default dftExport;

This issue is tracking a request to implement some way to avoid the extra repetition.

The use case I'm looking at for this is to type and name a function that's a default export, without repeating the function name:

export interface MyComponentProps {
   yes: boolean;
}
export default function MyComponent(props) {

} satisfies SFC<MyComponentProps>;

@ackvf's syntax suggestion seems like the best proposed so far.

export default: React.FC<{withIcon: boolean}> (({ withIcon, children }) => {
  return <SomeJSX withIcon={withIcon}>{children}</SomeJSX>
})

Would that be difficult to implement?

export default: {type} {value}

I guess another option could be to allow "default" as a variable name to export.

export const default: React.FC<MyProps> = function MyComponent(props) { ... }

This is type checked:

import type { Config } from "package";

export default (): Config => {
	return {
		properties: true
	};
};

@NikolaRHristov Yes, but this works only if you export a function, you are typing its return type.

Re: #13626 (comment)

I think this works well enough with satisfies?

type MyType = { name: string }

export default { name: 5 } satisfies MyType;
// Type 'number' is not assignable to type 'string'.(2322)

export default { name: "world", foo: 4 } satisfies MyType;
// Object literal may only specify known properties, and 'foo' does not exist in type 'MyType'.(1360)

export default { name: "world" } satisfies MyType;
// OK

Will satisfies allow typescript to infer argument and return types in something like the following?

export interface MyComponentProps { foo: string }
export default function MyComponent(props) {  return <> ... </>; }  satisfies React.SFC<MyComponentProps>;

@dobesv #13626 (comment)

type RequestHandler = (req: string, res: number) => string;

export default ((req, res) => {
   return 1;
}) as RequestHandler;
// Conversion of type '(req: string, res: number) => number' to type 'RequestHandler' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
//   Type 'number' is not comparable to type 'string'.(2352)
// and the whole block is highlighted as error.

export default ((req, res) => {
   return 1;
}) satisfies RequestHandler;
// Type '(req: string, res: number) => number' does not satisfy the expected type 'RequestHandler'.
//  Type 'number' is not assignable to type 'string'.(1360)
// but "RequestHandler" is highlighted as error (I would expect the return value to be highlighted).

Playground Link

I think that satisfies this request to my satisfaction, in that case.

@sid Vishnoi It works on function declarations too.

export default (function (req, res)  {
   return 1;
}) satisfies RequestHandler;

Playground Link

Just want to point out that using both as and satisfies might actually be the best solution here until we get actual syntax. satisfies checks that the value conforms to the type, as forces the type instead of allowing TS infer its own type

type Union = { v: 1 | 2 }
export default { v: 1 } satisfies Union as Union; // Checked against Union and Union is preserved as the type of the export.

Playground Link

Thanks to @acutmore for suggesting this use of satisfies T as T

Another workaround is to pass the export value to an identity function to check its value:

function verify<T>(t: T): T { return t }

export default verify<React.FC<IProps>>(function(props) {
  // ...
})

I'm not sure if there's a canonical identity function that'd avoid introducing your own.

ekcom commented

For now, we continue to export default exportDefault; with something like const exportDefault: webpack.Configuration = {}; .

  • as induces type coercion
  • satisfies is a type assertion. It does not type the variable (even though it will validate it)

satisfies is a type assertion. It does not type the variable

Oh, too bad. I thought an earlier commenter said that it did type it. We haven't upgraded to the version of typescript with this yet, but this was one thing I was looking forward to.

You can also declare a typed variable and assign it in your export.

type Union = { v: 1 | 2 };

let d: Union;

export default d = {
  v: 1,
};

@mccallofthewild Then it could be reassigned.

@mon-jai No. 😁 That's not how assignment works.

let a;
let b = a = 1;
a = 2;
console.log(b == a);
// `false`

Shouldn't the following code work?

// d.js
let d: number;
export default d = 1:

// Other files
import d from "d.js";
d = 2;

satisfies T as T doesn't reduce boilerplate in all circumstances. eg using a type from a value doesn't work at all:

export default {some:'value'} satisfies SomeType<typeof SomeValue> as SomeType<typeof SomeValue>

and even if it did work, I'm repeating my type declaration twice.

Doing export default: SomeType<typeof SomeValue> {some:'value'} would be great for reducing boilerplate

MrBns commented

from 2017 -> 2024... Still this issue is open. wow.

@MrBns Some time ago, @RyanCavanaugh proposed an initiative to deal with persistent issues - #54923. You might want to ask @RyanCavanaugh to add this issue to the list for upcoming discussions.

@a-tarasyuk unfortunately, in the announcement of that initiative they include:

Requesting Feature Updates

For the foreseeable future, we'll be working off the "most-upvoted" suggestion list. Please don't post comments in issues or iteration plans asking for feature updates. Once the top suggestions have been cleared out, we might change this policy, possibly by having a survey or something.

I don't see anything indicating that has changed, but it's possible I missed it

With that being said, if this issue is considered for a Feature Update, whoever is considering it, please note that #56235 (comment) (export default ... satisfies as T) is a possible solution to this issue and many others