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

Require "as const"? #47756

Open
5 tasks done
tolmasky opened this issue Feb 7, 2022 · 5 comments
Open
5 tasks done

Require "as const"? #47756

tolmasky opened this issue Feb 7, 2022 · 5 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@tolmasky
Copy link

tolmasky commented Feb 7, 2022

Suggestion

I have a factory function that takes a specification in the form of an object literal and returns a class. If the description passed in is a literal and annotated with as const, then the static properties it takes from the specification retain their exact types (like strings for instance), vs. the more generic "string" type:

type Specification = { readonly type: string; };

function SourceNode<T extends Specification>(props: T) { 
    return { ...props }; // simplified to show a reduction.
};

export const Statment = SourceNode({
    type: "Statement"
} as const);

// This statically knows it will never enter this if
if (Statement.type === "pizza") {
}

However, if you forget as const, all of this of course falls apart. I'd like a way to immediately surface an error if you don't pass an as const object literal:

// Would be nice if this yelled at you, or maybe could somehow infer that it's const since it's never used again?
export const Statement = SourceNode({
    type: "Statement"
});

I've tried every manner of annotation but haven't been able to accomplish this, but would love to know if there is a way to do this currently:

function SourceNode<T extends Readonly<Specification>>(props: T)  { readonly [key in keyof T]: T[key] } { 
    return { ...props } as const;
};

🔍 Search Terms

as const, required, const annotation

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@whzx5byb
Copy link

whzx5byb commented Feb 7, 2022

A workaround from #30680 (comment)

@jcalz
Copy link
Contributor

jcalz commented Feb 7, 2022

Is the underlying use case here the same as #30680? I would think you'd rather see SourceCode({type: "Statement"}) have "Statement" be inferred as a string literal type. Or would you really prefer an error like:

type Specification = { readonly type: string; };

function SourceNode<T extends (string extends T["type"] ? 
  { readonly type: "OH NOES YOU FORGOT as const" } : Specification)>(props: T) {
  return { ...props }; // simplified to show a reduction.
};

export const BadStatement = SourceNode({
  type: "Statement" // error, Type '"Statement"' is not assignable to type '"OH NOES YOU FORGOT as const"'.
});

export const Statement = SourceNode({
  type: "Statement" // okay
} as const);

I suppose if someone writes SourceNode(somethingCreatedEarlier) then any chance of inferring something narrower is gone, at which point you'd maybe still want an error. If so you can probably do something like this yourself, at least as a workaround until and unless this feature is implemented:

// I don't know, something like this
type Const<T> = T extends object ? { readonly [K in keyof T]: Const<T[K]> } :
  T extends number ? number extends T ? 0 : T :
  T extends string ? string extends T ? "" : T : T;

function foo<T>(x: Const<T>) { }

const oops = [123]
foo(oops) // error, number[] not assignable to readonly 0[]

const okay = [123] as const;
foo(okay) // okay

Playground link

@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Feb 7, 2022
@RyanCavanaugh
Copy link
Member

I'm not 100% clear on what the thing being asked for is - if you wrote

let s = "ok".
SourceNode({ type: s } as const);

then the implicit contract is violated while the syntactic one is met. Conversely you could write

const s: { type: "statement } = { type : "statement" };
SourceNode(s);

and the implicit contract is met while the syntactic one is violated.

Programming languages generally don't create constructs that forbid indirection - I don't think we'd ever add something that says that the syntactic form you pass to a function has to be a particular thing. Framework-specific linters do exist (e.g. React's hooks lint) and might be more appropriate.

@tolmasky
Copy link
Author

tolmasky commented Feb 7, 2022

Programming languages generally don't create constructs that forbid indirection

In some sense, I am kind of trying to write a syntax construct, and thus the lack of indirection would be fine, "the same way" you can't do import pathVariable, and are instead forced to use a string literal with import statements. I am attempting to make a class factory, so I wish I could just do SourceNode Statement { }...

I'm not 100% clear on what the thing being asked for is - if you wrote

The responses above have actually been super helpful and given me plenty to experiment with. To get to the heart of the matter as to why I wouldn't just use a generic for this for example, is because it seems like any information I want to access both at runtime and type-check time needs to be expressed twice. I wish I could just treat exact strings kind of like in C++ templates:

type SourceNode<Name extends exact string> = class
{
    static type: Name = Name;
    type: Name = Name;
};

However, I can't (to my knowledge) assign a value to the value of an exact string (nor can I assign types to static members of course). But if the above was possible, I could avoid having to do something like:

class Statement extends SourceNode<"Statement"> 
{
    static type = "Statement"; // Ugh.
    type = "Statement";
}

My workaround is to have a class factory that piggy-backs off of this sort of literal type inference to have my cake and eat it too:

interface SourceNodeSpecification
{
  type: string;
};

const SourceNode = (specification: SourceNodeSpecification) =>
  class
  {
      static type = specification.type;
      type = specification.type;
  };

That way I can now do:

const Statement = SourceNode
({
    type: "Statement";
} as const);

function doStuff(node: Statement | Declaration)
{
    if (statement.type === "Statement")
    {
        // Must be a Statement.
    }
    else
    {
        // Must be a Declaration.
    }
}

And then have all my nodes I instantiate have the correct type without having to pass in a string anywhere else in the program.

@hreinhardt
Copy link

@tolmasky
Is this issue still relevant? TS 5.0 introduced const type-parameters so you could do something like:

function SourceNode<const T extends Specification>(props: T) { 
    return { ...props }; // simplified to show a reduction.
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants