Repository navigation
Proposal: A way to infer unique symbol and enum member #22118
Description
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)
- 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, juststringtype), 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'
Activity
sylvanaar commentedon Feb 22, 2018
Took me a minute to figure out what all you were asking for - it seems like at least a couple things:
Semi-Related: #10195
Also really good: #10676
Jack-Works commentedon Feb 22, 2018
#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 Jon S Akhtar (@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 commentedon Feb 22, 2018
So if some of the concepts are out of scope, only preserve 1, 2, 3, 7, 8 in the above example.
mhegazy commentedon 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 commentedon Feb 23, 2018
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
[-]Proposal: A way to require a literal type[/-][+]Proposal: A way to infer unique symbol and enum member[/+]Jack-Works commentedon Feb 24, 2018
Jon S Akhtar (@sylvanaar) s-ve Mohamed Hegazy (@mhegazy)
Sorry, I have verified the comments and updated my proposal.
sylvanaar commentedon Feb 27, 2018
Jack Works (@Jack-Works)
By the way, have you ever looked at the action creator library I use:
https://github.com/piotrwitek/typesafe-actions
mhegazy commentedon Jul 12, 2018
For 3:
The use of
Tin the output tells the compiler that the type needs to be passed along, think identity function. it would be very strange iffn(x)does not have the same type asx.what is happening here is that since
cis a const, it keeps the un-widened type ofC.A. if the intention is thatchas the typeC, you can always give it an explicit type annotation:For 7:
the type of
fn(Symb)is aunique symbol, the issue is it is widened before being assigned tog. you can get around this by givinggan explicit type.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, sincecis aconstthen 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 ifSymbol()is used inside a loop, so it fails on the side of permissiveness, removing theunique symbolunless it is a pattern of the formconst s = Symbol();.Jack-Works commentedon Jul 13, 2018
5 months passed, I've reconsidered this proposal.
For 3:
Is there a way, just like
T extends stringcan 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 SymbolorT extends unique symbol?Oh... Widened before assignment ... Does it become impossible?
mhegazy commentedon Jul 13, 2018
T extends number.Jack-Works commentedon Jul 17, 2018
Okay, so what about the
unique symbol?mhegazy commentedon 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 commentedon Jul 18, 2018
Thank's for your replies