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

[Feature request] allow use as const + type or interface #31062

Open
2 of 5 tasks
bluelovers opened this issue Apr 22, 2019 · 18 comments
Open
2 of 5 tasks

[Feature request] allow use as const + type or interface #31062

bluelovers opened this issue Apr 22, 2019 · 18 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@bluelovers
Copy link
Contributor

Search Terms

Suggestion

Use Cases

allow use as const + type or interface

so we can make sure as const is follow type or interface

Examples

check as const output is follow IVueCliPrompt[]

.ts

interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

const prompts = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
] as const IVueCliPrompt[];

export = prompts

.d.ts

declare const prompts: readonly [{
    readonly name: "replaceFiles";
    readonly type: "confirm";
    readonly message: "Replace current files with preset files?";
    readonly default: false;
}];
export = prompts;

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh
Copy link
Member

I can't really tell what you're trying to do with this

@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Apr 23, 2019
@bluelovers
Copy link
Contributor Author

when miss default field, will show type error , because it need follow IVueCliPrompt

and at end, it will out like normal as const, only do type chk make sure prompts is follow IVueCliPrompt[] before emit, but output as

declare const prompts: readonly [{
    readonly name: "replaceFiles";
    readonly type: "confirm";
    readonly message: "Replace current files with preset files?";
    readonly default: false;
}];

@NN---
Copy link

NN--- commented Apr 23, 2019

@bluelovers
AFAIU You suggestion is to allow skipping the type from the left side.
Given

interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

Instead of writing explicitly ReadonlyArray<Readonly>:

const prompts: ReadonlyArray<Readonly<IVueCliPrompt>> = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
];

You want 'as const Type' to behave as the 'Type' was written in the left side.

const prompts = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
] as const VueCliPrompt[]; // Same effect as previous code sample

Correct ?

@bluelovers
Copy link
Contributor Author

im not wanna do a prompts: ReadonlyArray<IVueCliPrompt>


as const VueCliPrompt[]; mean do type chk VueCliPrompt[] before emit .d.ts

and output code like we do as const is a static value


do type check like as prompts: IVueCliPrompt[]

Error:(20, 2) TS2741: Property 'default' is missing in type '{ name: string; type: string; message: string; }' but required in type 'IVueCliPrompt'.

.ts

interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

const prompts: IVueCliPrompt[] = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
	{
		name: 'replaceFiles2',
		type: 'confirm',
		message: 'Replace current files with preset files?',
	},
];

export = prompts

but output declaration (.d.ts) like as const

so this make sure our as const is not miss anything and
compatible with IVueCliPrompt[]

.ts

const prompts = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
] as const;

=>

.d.ts

declare const prompts: readonly [{
    readonly name: "replaceFiles";
    readonly type: "confirm";
    readonly message: "Replace current files with preset files?";
    readonly default: false;
}];
export = prompts;

at end

interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

const prompts = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
	{
		name: 'replaceFiles2',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		// error here => Error:(20, 2) TS2741: Property 'default' is missing in type '{ name: string; type: string; message: string; }' but required in type 'IVueCliPrompt'.
	},
] as const IVueCliPrompt[];

export = prompts

=>

Error:(20, 2) TS2741: Property 'default' is missing in type '{ name: string; type: string; message: string; }' but required in type 'IVueCliPrompt'.


interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

const prompts = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
	{
		name: 'replaceFiles2',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
] as const IVueCliPrompt[];

export = prompts

=>

.d.ts

declare const prompts: readonly [{
    readonly name: "replaceFiles";
    readonly type: "confirm";
    readonly message: "Replace current files with preset files?";
    readonly default: false;
}, {
    readonly name: 'replaceFiles2',
    readonly type: 'confirm',
    readonly message: 'Replace current files with preset files?',
    readonly default: false
},];
export = prompts;

@greg-hornby-roam
Copy link

I believe he wants to use the as const feature while still type checking that the structure matches an interface. A workaround I'd use for something like that would be

interface ITest {
  a: number;
  b: string;
}

let foo = {
  a: 5,
  b: "Hello"
} as const;

<ITest>foo; //a useless javascript line, but checks the var `foo` is assignable to `ITest`

@bluelovers
Copy link
Contributor Author

@Gregroam yes

@DanielRosenwasser DanielRosenwasser added In Discussion Not yet reached consensus Suggestion An idea for TypeScript and removed Needs More Info The issue still hasn't been fully clarified labels Apr 24, 2019
@nattthebear
Copy link

<ITest>foo; //a useless javascript line, but checks the var `foo` is assignable to `ITest`

But that's a type assertion, not check. Wouldn't it be closer to

const __unused: ITest = foo;

@bluelovers
Copy link
Contributor Author

a bug? here

should show type error, but didn't

interface IVueCliPrompt
{
	name: string,
	type: 'confirm' | string,
	message: string,
	default: unknown
}

const prompts: IVueCliPrompt = [
	{
		name: 'replaceFiles',
		type: 'confirm',
		message: 'Replace current files with preset files?',
		default: false
	},
	{
		name: 'deps-cross-fetch',
		type: 'confirm',
		message: 'Add cross-fetch?',
		default: false
	},
] as const;

export = prompts

@AnyhowStep
Copy link
Contributor

This StackOverflow question can be solved with this,
https://stackoverflow.com/questions/57069802/as-const-is-ignored-when-there-is-a-type-definition

@Harpush
Copy link

Harpush commented Jan 9, 2020

Anything mew about this feature? Will really help defining const literals with typed structure

@RyanCavanaugh
Copy link
Member

All of our planning documents are public; please don't ping for news or mews.

@ondratra
Copy link

ondratra commented Mar 1, 2020

I believe he wants to use the as const feature while still type checking that the structure matches an interface. A workaround I'd use for something like that would be

interface ITest {
  a: number;
  b: string;
}

let foo = {
  a: 5,
  b: "Hello"
} as const;

<ITest>foo; //a useless javascript line, but checks the var `foo` is assignable to `ITest`

Unfortunately, this doesn't work as expected.
You can check an empty object <ITest>{} or <ITest>{b: 'property "a" is missing in this object'}, and it doesn't throw an error (tested with TypeScript v3.8.3).

const foo2 = {} as const
const foo3 = {
  b: 'property "a" is missing in this object'
} as const;
const foo4 = {
  newProperty: '122',
} as const;
const foo5 = {
  newProperty: '122',
};
<ITest>foo2; // doesn't throw an error
<ITest>foo3; // doesn't throw an error
<ITest>foo4; // does throw an error: Conversion of type '{ readonly newProperty: "122"; }' to type 'ITest' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ readonly newProperty: "122"; }' is missing the following properties from type 'ITest': a, b
<ITest>foo5; // does throw an error: Conversion of type '{ newProperty: string; }' to type 'ITest' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Type '{ newProperty: string; }' is missing the following properties from type 'ITest': a, b

This StackOverflow question can be solved with this,
https://stackoverflow.com/questions/57069802/as-const-is-ignored-when-there-is-a-type-definition

There was no solution proposed using only the type system. They both need defining a variable (or a function) that is useless during runtime.

@liquidg3
Copy link

I have a use-case where I think this would be helpful.

Our Schema module (WIP: https://developer.spruce.ai/#/schemas/index) is built to standardize data structures/models across our ecosystem.

Our CLI tool generates types/interfaces/protocols for different languages based on these definitions.

Because we write our definitions in typescript, it would be so cool if we didn't have to rely on generated files to create types based on those definitions.

Here is a snippet pulled and modified from one of our tests that I can't get to work because the widening that happens:

//buildSchemaDefinition= <T extends ISchemaDefinition>(definition: T) => definition
const userDefinition = buildSchemaDefinition({
	id: 'select-union-test',
	name: 'select union test',
	fields: {
                name: { type: FieldType.Text },
		favoriteColor: {
			type: FieldType.Select,
			options: {
				choices: [
					{
						value: 'blue',
						label: 'Blue'
					},
					{
						value: 'red',
						label: 'Red'
					}
				]
			}
		}
	}
})



type SelectUnion = 'blue' | 'red'

const user = new Schema(userDefinition, { favoriteColor: 'blue' })

// favorite colors should be a union of all choices[number]['value'], so should match SelectUnion
// instead it's widened to a string (allowing favoriteColor to be set to anything without warning) 
const favColor: SelectUnion = user.get('favoriteColor')

t.is(favColor, 'blue')

I tried doing putting read only and as const and I can't get it to work without a generated the file that essentially does.

const userDefinition = buildSchemaDefinition<{.... definition ...}>({... definition a second time ...})

// now there is no widening, so I can map it really helpful ways
const user : SchemaDefinitionValues<typeof userDefinition> = {
    name: 'Tay',
    favoriteColor: 'wrong' // not one of 'blue' | 'red'
}

Now I can import the definitions and work in them in real time and have my "value objects" check themselves (so changing the choices of a universal definition of a person in our platform will immediately fail lint).

The type mapping is here: https://github.com/sprucelabsai/spruce-schema/blob/dev/src/Schema.ts#L52

Thanks!

@guilhermetod
Copy link

I wonder if #32758 would end up solving this, or maybe this would have to be solved in order to get that issue done.

@steverep
Copy link

I also see a big need for this feature. To add to the reasoning:

  1. There is no way to get IntelliSense on the object or array literal you are defining, which is frustrating for large arrays of objects with many properties
  2. Yes the literal object or array will probably get type checked downstream when used in a function call or another assignment, but that may be in a different file and not caught until a full lint is run, which just adds to the developers workload

My idea on how to make this work is just to mimic the readonly keyword and utility type with const or a new literal keyword. It would behave identical to readonly with the added restriction of never widening the type after assignment:

const myPets = [{...}, {...}, ...] as Const<Pet[]>;
const myPets: Const<Pet[]> = [{...}, {...}, ...];
// or
const myPets = [{...}, {...}, ...] as Literal<Pet[]>;
const myPets: Literal<Pet[]> = [{...}, {...}, ...];

@steverep
Copy link

i think new satisfies keyword is done this issue

I disagree. It's a step in the right direction, but if you add as const to the playground example, you get the same error as trying to type the object first:

'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.

So the type will be checked, but the resulting type is still widened up to the primitives instead of being literals.

@steverep
Copy link

I filed #51173 as a bug against the satisfies operator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests