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

Proposal: A way to infer unique symbol and enum member #22118

Closed
Jack-Works opened this issue Feb 22, 2018 · 13 comments
Closed

Proposal: A way to infer unique symbol and enum member #22118

Jack-Works opened this issue Feb 22, 2018 · 13 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@Jack-Works
Copy link
Contributor

Jack-Works commented Feb 22, 2018

Thanks to @s-ve ,I've resolved my questions. But there is still something we can discuss.
Let's focus on Example 3 and 7.

// 3.
enum _C { A, B, C }
const c = fn(_C.A) // can not express if I want to infer this constant to have type _C.A (Now `_C`)

// 7. 
function fn<T extends Symbol>(x: T): T {return x}
const Symb = Symbol()
const g = fn(Symb) // expected to have type `unique symbol` refer to Symb but `symbol`

How to write type if you want a literal generics

// DO
const ActionCreator = <T extends string, P = any>(type: T) => (payload: P) => ({ type, payload });
const TodoAddOne = ActionCreator("todo.add");

// DONT
const ActionCreator = <T, P = any>(type: T) => (payload: P) => ({ type, payload });
const TodoAddOne = ActionCreator<"todo.add">("todo.add");

Content below is useless now, I've learnt the correct way to write types I want(See above).


Search Terms: type string literal

In some scenarios, we need to get type inference by a sure string, but not a string type.
(Most famous one is ActionCreator in Redux, but not the only one.)

Now, typescript can infer the type of (function <T>(x: T): T {return x})('hello') is the string, but if we want to get a more precise infer, it seems no way to do this.
I'm sorry for my ignorance, typescript actually can do this.
I'll show how in my following examples.

This is not a formal language feature proposal. But a demo one is enough to explain what I mean.
This how ActionCreator works now:

// Before (Don't)
const ActionCreator = <T, P = any>(type: T) => (payload: P) => ({type, payload})
const TodoAddOne = ActionCreator<'todo.add'>('todo.add')

// After(Do)
const ActionCreator = <T extends string, P = any>(type: T) => (payload: P) => ({ type, payload });
const TodoAddOne = ActionCreator("todo.add");

Now TodoAddOne has type (payload: any) => { type: "todo.add"; payload: any; }

function ActionCreator<Payload, literal Action>(type: Action){
    return (payload: Payload) => ({ type, payload })
}
const TodoAddOne = ActionCreator<{add: number}>('todo.add')

With no duplication of todo.add, we get the same type as above.
Though this is a small reduction, it goes useful when actions get greater.

How do I think this should work? (NO, Skip this section)

  1. Since who wrote the code choose to use a literal type, if Typescript cannot infer the precise type of it(not the string I want, just string type), Typescript should emit an Error.
const fn = <literal T>(a: T) => a
// 1. Direct infer (ts can do this)
const a = fn('hey') // has type 'hey'

// 2. Direct infer (number) (ts can do this)
const b = fn(2) // has type 2

// 3. Direct infer (enum) (No, ts cannot do this)
enum _C { A, B, C }
const c = fn(_C.A) // has type _C.A

// 4. Infer from constant (Rejected proposal)
const _d_name = 'nyaa'
const d = fn(_d_name) // has type 'nyaa'

// 5. Infer from **simple** string operation (Rejected proposal)
const _e_name = 'prefix~'
const e = fn(_e_name + 'hello') // has type 'prefix~hello'

// 6. Infer from another file (if possible) (Rejected proposal)
import { OH_MY_LITTLE_PONY } from './constant'
// export const OH_MY_LITTLE_PONY = 'MyLittlePony' in 'constant.ts'
const f = fn(OH_MY_LITTLE_PONY) // has type 'MyLittlePony'

// 7. Infer from Symbol() (No, ts cannot do this, it has type `symbol` now)
const Symb = Symbol()
const g = fn(Symb) // has type `unique Symbol`

// 8. Report error if compiler cannot infer the type (Rejected proposal)
const _h: any = 'hello'
const h = fn(_h)
//        ~~~~~~
// `any` is incompatiable with type `literal T`.
// You must provide a presice type for `literal T`

// 10. Maybe a way to bypass the error? (Useless now since 8 is rejected.)
const q: any = 0
fn(q!) // has type any

// 11. Not in generics (// Well, this case works when using 'const' but not 'let', that's reasonable.)
const n: literal string = 'okay' // has type 'okay'
@sylvanaar
Copy link

sylvanaar commented Feb 22, 2018

Took me a minute to figure out what all you were asking for - it seems like at least a couple things:

  1. Infer the literal from contextual types (Which I think they have rejected in the past) (Maybe Premature literal type widening? #17363)
  2. Infer literals from expressions which can be calculated at compile time. Infer literal types for string concatenations and similar expressions #13969

Semi-Related: #10195
Also really good: #10676

@Jack-Works
Copy link
Contributor Author

#10195 seems good but might not work well with code formatter

@s-ve 's answer seems definitely resolve my problem, I will verify it later. I'm felling strange that why there is no article about typed redux I've been seen noted or use this technique.

As @sylvanaar said, some of concept I've asked is already rejected, that's okay, the core concept here is a way(if there is none) to explicitly let complier infer a literal as-is. That fullfil the most of use cases.

Later today try to build a ActionCreator on @s-ve 's way, if success, I think this issue can be closed

@Jack-Works
Copy link
Contributor Author

So if some of the concepts are out of scope, only preserve 1, 2, 3, 7, 8 in the above example.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 22, 2018

I do not think 8 is a thing we can do. any is the wildcard, it matches any constraint in the system. think of it as a union of all possible type.

@Jack-Works
Copy link
Contributor Author

Umm, I know that any is a union type of all possible type, but I want to reject an infinity union since who write "literal" is actually want a certain type. This is useful in type narrowing and it mostly likes a mistake if an any is provided.

So if reject any is also impossible, let's just ignore it.

And for #10195, let's extend it to another case

// 11. Not in generics
const n: literal string = 'okay' // has type 'okay'

@Jack-Works Jack-Works changed the title Proposal: A way to require a literal type Proposal: A way to infer unique symbol and enum member Feb 24, 2018
@Jack-Works
Copy link
Contributor Author

@sylvanaar @s-ve @mhegazy

Sorry, I have verified the comments and updated my proposal.

@sylvanaar
Copy link

@Jack-Works

By the way, have you ever looked at the action creator library I use:

https://github.com/piotrwitek/typesafe-actions

@mhegazy
Copy link
Contributor

mhegazy commented Jul 12, 2018

For 3:

The use of T in the output tells the compiler that the type needs to be passed along, think identity function. it would be very strange if fn(x) does not have the same type as x.

what is happening here is that since c is a const, it keeps the un-widened type of C.A. if the intention is that c has the type C, you can always give it an explicit type annotation:

// 3.
function fn<T>(x: T): T { return x }
enum _C { A, B, C }
const c:C = fn(_C.A);

For 7:

the type of fn(Symb) is a unique symbol, the issue is it is widened before being assigned to g. you can get around this by giving g an explicit type.

function fn<T extends Symbol>(x: T): T { return x }
const Symb = Symbol()
const g: typeof Symb = fn(Symb);

In general, the issue here is the compiler deciphering the intent of the user. The intent can be ambiguous, for instance const c = fn(C.A);. The current design fails on the side of keeping the literal type as long as possible, since c is a const then the literal type persists. Symbols are a bit different, since they are generated at run time from a special expression (Symbol()), the compiler, again, can not tell all the time the intent, for instance if Symbol() is used inside a loop, so it fails on the side of permissiveness, removing the unique symbol unless it is a pattern of the form const s = Symbol();.

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jul 12, 2018
@Jack-Works
Copy link
Contributor Author

5 months passed, I've reconsidered this proposal.
For 3:
Is there a way, just like T extends string can infer literal string, can infer the literal of the enum member?
But there is nothing as string as a base type for enum, I can't just write T extends enum (since enum is a reversed in TS maybe we can add it?)

For 7:
The same, can we use T extends Symbol or T extends unique symbol?

Oh... Widened before assignment ... Does it become impossible?

@mhegazy
Copy link
Contributor

mhegazy commented Jul 13, 2018

Is there a way, just like T extends string can infer literal string, can infer the literal of the enum member?

T extends number.

@Jack-Works
Copy link
Contributor Author

Okay, so what about the unique symbol?

@mhegazy
Copy link
Contributor

mhegazy commented Jul 17, 2018

as i mention earlier in #22118 (comment), the issue is not the inference, the issue is widening on the const. so you want the constant to have an explicit type annotation.

@Jack-Works
Copy link
Contributor Author

Thank's for your replies

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants