Typescript: Support Const Type Constraint

Created on 15 Oct 2020  ·  11Comments  ·  Source: microsoft/TypeScript

Search Terms

Const Type Constraint Literal

Suggestion

Add new syntax const T or a global generic type Constant<T = any> = T extends const T ? T : never;

type ConstString = const string is same as type ConstString = Constant<string>.

  • If T extends string | number | boolean | symbol | bigint | null | undefined, requires T to be a constant.
  • If T is object or Array, requires every member of T to be a constant.
  • If T is enum, nothing to do, because enum is same as const enum

The difference with union literal is that type 'foo' | 'bar' can only be 'foo' or 'bar', but type const string can be any constant string.

Use Cases

  • Sometimes we want to constrain a type or a parameter to be literal.
  • It can help the TS compiler to recognize constant types better

Examples

let foo = 'foo'; // string
const bar= 'bar'; // 'bar'

const notConstant: const string = foo; // error
const trulyConstant: const string = bar; // success

type Baz = { baz: string } ;
const baz1: const Baz = { baz: 'baz' }; // success
const baz2: Baz = { baz: 'baz' }; // not a constant
const baz3: Baz  = { baz: 'baz' } as const; // current available, same as baz1

type ConstArray<T> = const Array<T>;
// same as
type AnotherCosntArray<T> = Array<const T>;

function tuple<T extends ConstArray>(...args: T) {
  return args;
}
const t1 = tuple(foo); // error!
const t2 = tuple(bar); // success! typeof t2 should be ['bar']
const t3 = tuple('a', 1, true, ['1'], baz1) // ['a', 1, true, ['1'], { baz: 'baz' }];
const t4 = tuple(baz2) // error!

let variable: const string = 'variable'; // this is possible
variable = 'another variable'; // success
variable = document.title; // error

let reactiveObject: const { foo: string } = { foo: 'foo' }; // success;
reactiveObject.foo = 'bar' // success

In fact, const only affects the assignment behavior, and a const type is considered to be the same as a non const type when read it.
A const type doesn't mean immutable, just means every primitive value should be literal, so maybe should call it literal type.

Alternatives

  • Only add extends const syntax, and add global generic type Constant<T = any> = T extends const ? T : never;
  • Use finally or static or literal(not exist in current keywords) keyword instead of const
  • Use type Literal instead of type Constant.

Related issues

30680 Suggestion: Const contexts for generic type inference

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Awaiting More Feedback Suggestion

Most helpful comment

The main use case––seems to be––to help library developers ensure that consumers are passing narrowed objects:

// BAD
acceptsNarrowed({
  a: "A",
  b: true,
  c: 3,
});

// GOOD
acceptsNarrowed({
  a: "A",
  b: true,
  c: 3,
} as const);

This feature would really shine! I've run into this problem many times. The following would be incredibly useful to me personally.

declare function acceptsNarrowed<O extends Constant<Record<PropertyKey, any>>>(o: O): void;
// or
declare function acceptsNarrowed<O extends const Record<PropertyKey, any>>(o: O): void;

All 11 comments

Sometimes we want to constrain a type or a parameter to be literal.

Can you provide a real world example where this would be useful?

Sometimes we want to constrain a type or a parameter to be literal.

Can you provide a real world example where this would be useful?

function tuple<T extends Array<string | number>>(...args: T) {
  return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar';
const t2 = tuple('foo', bar); // ['foo', string]

For intellisense, parameter must be literal.

Why not to use just:

function tuple<T extends Array<string | number>>(...args: T) {
  return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar' as const;
const t2 = tuple('foo', bar); // ['foo', 'bar']

?

Why not to use just:

function tuple<T extends Array<string | number>>(...args: T) {
  return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar' as const;
const t2 = tuple('foo', bar); // ['foo', 'bar']

?

tuple may come from package.
The main goal is to constrain function parameters must be literal.
Now we have no way to diff const type and variable type.

The main goal is to constrain function parameters must be literal.

But you didn't provide an example where this would be useful.

This could be accomplished with the conditionally assignable types discussed in #40779.

type StrictSubtype<T> = match U extends T => T extends U ? false : true;
type ConstString = StrictSubtype<string>;

The main use case––seems to be––to help library developers ensure that consumers are passing narrowed objects:

// BAD
acceptsNarrowed({
  a: "A",
  b: true,
  c: 3,
});

// GOOD
acceptsNarrowed({
  a: "A",
  b: true,
  c: 3,
} as const);

This feature would really shine! I've run into this problem many times. The following would be incredibly useful to me personally.

declare function acceptsNarrowed<O extends Constant<Record<PropertyKey, any>>>(o: O): void;
// or
declare function acceptsNarrowed<O extends const Record<PropertyKey, any>>(o: O): void;

The main goal is to constrain function parameters must be literal.

But you didn't provide an example where this would be useful.

I have to agree with @harrysolovay. I run into problems around this regularly, usually with some complex type-converting breaking down when it resieves non-literal string or number (especially when using the upcoming TempateString-literal type).

Also, i think this could solve many of the issues related to the notoriously strange symbol type. just to mention a few:

  • precise symbol being returned from a function
  • specifying a precise symbol in function parameters
  • dynamic object keying with symbols
  • symbol indexing

This would make it possible to give symbols to an API that can be referenced at a later point, both internally and by the API consumer.

// has the incredibly useless returntype of `{[x: string]: string }`
function someApi <T extends symbol>(someSymbol: T) {
  return {[someSymbol]: 'dynamic symbol key'}
}

// return type is `symbol`, making it impossible to reference it again in the typeSystem.
function GetDynamicallyUniqueSymbol () {
  const sym = Symbol('unique')
  return sym
}

After a few thinking, I think there are still some unresolved problems in the proposal.


Object and Array type probably shouldn't extends const to every member, because of prototype.

const foo = (arr: Constant<Array<string>>) => arr;

If object member extends const, Array.prototype.length will be const, so this function cannot be called.

Use this instead.

const bar = (arr: Array<Constant<string>>) => arr;

What about funciton type? Seems function type should not be affected by const.

We need more discussion.


This could be accomplished with the conditionally assignable types discussed in #40779.

type StrictSubtype<T> = match U extends T => T extends U ? false : true;
type ConstString = StrictSubtype<string>;

@tjjfvi That will be a greet solution! I think generic is much better than a new syntax.

@woshiguabi

That will be a greet solution! I think generic is much better than a new syntax.

I have to be nitpickey here, but this is also new syntax. I also don't think this would address the issue in this thread. They do have overlap tho.

const foo = (arr: Constant<Array<string>>) => arr;

If object member extends const, Array.prototype.length will be const, so this function cannot be called.

I don't see a reason why this couldn't be called. I would interpret that as: the function only takes arrays with a known length (so eg. no string[]), but the specific length doesn't matter.
But it's good that you're brining these thing up.

What about funciton type? Seems function type should not be affected by const.

I have now idea how const should behave on a function directly. Maybe make everything that can be a litteral require a literal? But i don't think that would be a good idea as people could do the same with the individual params/return types. I don't think it should be usable on a function type, but it should stay similarly limited like a as const assertion.

There has been a want for something like this (or like the other proposal) for quite some time. There was a suggestion about extending the unique type to handle most of these things. I can't find the issue which suggested it right now, but it was quite popular and if i remember correctly, there were steps being taken to implement it.I think it was ultimatly shafted for some newly discovered technical reason.
If anybody knows which issue i'm talking about and happens to find it, it could potentially add quite a bit to these discussions. Might also be worth evaluating wether it could be implemented now, because it was discussed in length it could save us a lot of effort as would have to discuss very similar thing eventually

@KilianKilmister

I have to be nitpickey here, but this is also new syntax. I also don't think this would address the issue in this thread. They do have overlap tho.

😜Thanks much! I mean no need for another new syntax.

https://github.com/tc39/proposal-record-tuple
Notice this stage 2 proposal, maybe we will finally achieve our goal through this

Was this page helpful?
0 / 5 - 0 ratings