sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
14.24k stars 540 forks source link

Proposal: TypeToJSObject #133

Closed evantahler closed 2 months ago

evantahler commented 4 years ago

Hello!

It might be interesting to have a utility that allows us to get the Object-style notation for a type/interface and make it available for Javascript to see, as an object. This could be useful for generating documentation, error creation, or API documentation.

Something like:

interface Car {
  wheels: number;
  manufacturer: string;
  color: string;
  licensePlate: string;
}

console.log(`Car objects require: ${TypeToJSObject<Car>}`)

Upvote & Fund

Fund with Polar

sindresorhus commented 4 years ago

Is this actually possible?

evantahler commented 4 years ago

I don't know... but it would be very helpful!

There are tools like typedoc which somehow are able to build documentation sites from TS/TSX files...

papb commented 4 years ago

This is not possible; unless you think it is acceptable to have the function somehow read the original typescript source code and parse it. Note that it's even worse than just reading the file source code, you have to read the original TS source code. In most cases that file is not even available. For example, most npm packages written in typescript do not publish the TS source code as part of the package, only the compiled code.

The reason this is not possible otherwise is because TypeScript has full type-erasure, which implies in particular that a type signature cannot change the runtime behavior of a program (unless the program reads its own source code).

papb commented 4 years ago

However, turning this around is very possible. It is possible to have a JsObjectToType utility. Would that work?

evantahler commented 4 years ago

Thanks @papb! Doing it in reverse with something like JsObjectToType might be useful... if we could get an instance of the class using the type, we could probably get the same outcome:

interface Car {
  wheels: number;
  manufacturer: string;
  color: string;
  licensePlate: string;
}

const car = new Car()

console.log(`Car objects require: ${JsObjectToType(Car)}`)
papb commented 4 years ago

@evantahler That won't work as well... It is not possible to call new on an interface.

papb commented 4 years ago

I was talking about something like:

// Below is a normal object that exists at runtime (JS)
const CAR_TYPE_STRUCTURE = {
  wheels: 'number';
  manufacturer: 'string';
  color: 'string';
  licensePlate: 'string';
} as const;

console.log('Car objects require:', CAR_TYPE_STRUCTURE);

// Below is a type declared as a function of that object. The type only exists at compile-time.
type Car = JsObjectToType<typeof CAR_TYPE_STRUCTURE>;

function useCar(car: Car) {
  // ...
}

Note: I am saying this is possible, but I am not sure whether this is good or not :sweat_smile:

evantahler commented 4 years ago

Thanks for all the brainstorming on this @papb! Maybe if I share the specifics of my use case, it might spark some ideas.

I maintain Actionhero (https://www.actionherojs.com), and we've been going "all in" on Typescript as of late. Every Action is well-defined class:

import { Action } from "actionhero";

class SleepAction extends Action {
  constructor() {
    super();
    this.name = "sleepTest";
    this.description = "I will sleep for some amount of time";
    this.inputs = {
      sleepDuration: {
        required: true,
        formatter: (n) => parseInt(n),
        default: () => 1000,
      },
    };
  }

  async sleep (time: number) {
    await new Promise(resolve => setTimeout(resolve, time));
  }

  async run ({ params }) {
    await this.sleep(params.sleepDuration);
    return { success: true};
  }
}

I'm trying to figure out a way to determine the class of params as it enters the run method. We've already done validation and formatting on the user input to ensure that params.sleepDuration is a number, parsed with pareInt() and if it wasn't provided, will have a default of 1000. Ideally, there would be some way to add a type to this.inputs, perhaps a type:number, to tell the compiler that {params}: {params: { sleepDuration: number }}

Here's the full example of the action: https://github.com/actionhero/actionhero/blob/master/src/actions/sleepTest.ts

evantahler commented 4 years ago

related to https://github.com/actionhero/actionhero/issues/1400

papb commented 4 years ago

Ideally, there would be some way to add a type to this.inputs, perhaps a type:number, to tell the compiler that {params}: {params: { sleepDuration: number }}

This can be done nicely, I can help you with that. But first, this does not seem to be what you requested in the first post. If you just want to "tell the compiler about something", that is very doable. But I think what you seem to want is to give a helpful error message to the user, since you wrote:

console.log(`Car objects require: ${TypeToJSObject<Car>}`)

I will think about a way to provide an error message like this in a nice way and let you know.

evantahler commented 4 years ago

This can be done nicely, I can help you with that.

Thanks @papb! I'd love your help figuring out how to determine types from inputs. Let's move that conversation over to https://github.com/actionhero/actionhero/issues/1400. Thank you!

fregante commented 1 year ago

What you requested in your first post is not possible at all in TypeScript. In the future TS might support compilation plugins that generate code, but that's still not possible in type-fest. Even then, I think it's a terrible idea.

In https://github.com/sindresorhus/type-fest/issues/133#issuecomment-707401534 it looks like you're looking for zod: https://zod.dev

Example from its docs

import { z } from "zod";

const User = z.object({
  username: z.string(),
});

User.parse({ username: "Ludwig" });

// extract the inferred type
type User = z.infer<typeof User>;
// { username: string }