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

Augment Key during Type Mapping #12754

Closed
MeirionHughes opened this issue Dec 8, 2016 · 88 comments · Fixed by #40336
Closed

Augment Key during Type Mapping #12754

MeirionHughes opened this issue Dec 8, 2016 · 88 comments · Fixed by #40336
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined Domain: Mapped Types The issue relates to mapped types In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@MeirionHughes
Copy link

MeirionHughes commented Dec 8, 2016

Aurelia follows the convention that for any field foo, when foo is changed by the framework, then an attempt to call fooChanged() is made. There is (seemingly) no way to describe this convention as a type (automatically) via the Mapped Type functionality alone.

I would like to open discussion to the prospect of augmenting property keys during mapping.

For example: via arthimetic:

type Changed<T> = 
{          
   [ P in keyof T ] + "Changed" ?: Function;
}

class Foo{
  field:number;
}

let foo = <Foo & Changed<Foo>> new Foo();

foo.field = 10;

if(foo.fieldChanged)
  foo.fieldChanged();

in this use-case specifically it probably would require #12424 too, but that is beside the point here.

@mhegazy mhegazy added the Suggestion An idea for TypeScript label Dec 8, 2016
@yortus
Copy link
Contributor

yortus commented Dec 9, 2016

Another relevant example is bluebird's promisifyAll. E.g.:

type Before = {
    foo: (callback: (err, res) => void) => void
    bar: (x: string, callback: (err, res) => void) => void
}

// promisifyAll would map Before to After - note all keys have 'Async' appended:
type After = {
    fooAsync: () => Promise<any>;
    barAsync: (x: string) => Promise<any>;
}

let before: Before;
let after: After = promisifyAll(before);

@saschanaz
Copy link
Contributor

I think unusual complex type operations like this should be supported by programmatic type builder like #9883.

@DanielRosenwasser DanielRosenwasser added Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined Domain: Mapped Types The issue relates to mapped types labels Aug 3, 2017
@Nimelrian
Copy link

Nimelrian commented Dec 12, 2017

Digging this one out since I'm wrestling with a use case for this right now: Scoping redux actions.
I have an interface Action:

interface Action<Type extends string> {
  type: Type
}

to avoid clashes with (e.g.) third party libraries, I scope my actions, for example like this:

export const createScopedActionType = _.curry((scope: string, type: string) => `${scope}/${type}`);

const createActionInMyScope = createScopedActionType("MyScope");
const MY_ACTION = createActionInMyScope("MY_ACTION");

It is known at compile time that MY_ACTION will have a value of "MyScope/MY_ACTION" and such should be its type. Typescript however sees it as a string.

I could obviously assign the resulting value directly, but that would reduce maintainability since with the approach depicted above I can simply reuse my partially applied function. So I only have to define my scope once. If I were to use regular assignments, I would have to change multiple lines of code (and be in danger of missing an instance!) if the name of the scope would change.

A compile time evaluation of functions called with literals (if evaluatable, as in: no side effects, no parameter modification => pure functions) should yield the final type/value returned by the function call.

C++ has a similar concept with constant expressions (constexpr).

I'm not sure in how far Typescript would allow the implementation of such a feature, but it would be a great help in keeping code maintainable and modular.

@Retsam
Copy link

Retsam commented Apr 5, 2018

It might be nice to revisit this issue in light of the 2.8 enhancements. Specifically, I think this issue is the only thing prevent a good implementation of Bluebird.promisifyAll. It was mentioned previously in this thread, but I think return type inference was an important piece of the puzzle that was also missing, until now.

@markusmauch
Copy link

markusmauch commented May 22, 2018

In our library, a Component class has for each of it's private properties (here id) an associated public getter function:

class Component { 
    private _id: string;
    public get_id() { 
        return this._id;
    }
}

For each component we have an additional Properties interface that - in this case - has an id property:

interface Properties {
    id:? string;
}

There is a lot of error-prone redundancy in here which caused us a lot of trouble in the past. With the new conditional types feature of TypeScript 2.8 I would love to see a linq-style syntax in indexed types:

type Properties<T extends Component> =
{
    [ P in keyof T where P.startsWith( 'get_' ) select P.replace( 'get_', '' ) as K ]: ReturnType<T[K]>;
}                                                                                            |
                                                                                             ReturnType is defined in TypeScript 2.8  
let componentProperties: Properties<Component>;
componentProperties.id = "foo"; // string is fine
componentProperties.id = true; // error

@ddurschlag6river
Copy link

In Loopback, a DB's order clause is specified as:

{ order: 'field ASC' }

or

{ order: [ 'field1 DESC', 'field2 ASC' ] }

I think this feature is required for TS to type this field appropriately.

@sb-js
Copy link

sb-js commented Aug 8, 2018

This is very important for things like using Bluebird.js to promisify everything in a module, as @yortus and @Retsam mentioned previously to this.

Using keyof, it is now possible to transform the types of a module such as import fs from 'fs' which is passed to Promise.promisifyAll(...). Which means this can almost be typed automatically. (And I strongly disagree with @saschanaz, code generation is not the right tool for this job. We can do it automatically!)

The only thing missing for this to work is the feature requested by this issue: support [expression] + [string literal] expression in types. I don't think it would be too hard, and I would even be glad to start on it and make a pull request, if someone could point me in the right direction of where to get started!

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Aug 15, 2018
@charles-toller
Copy link

@sb-js Looks like this would be a good place to start, as you'd need to add a bit that would parse a plus sign after a mapped type parameter.

@sb-js
Copy link

sb-js commented Aug 21, 2018

@drsirmrpresidentfathercharles Thanks for the pointer. I will give it a shot this week.

@ZSkycat
Copy link

ZSkycat commented Aug 30, 2018

another case:

interface IActionsA {
    update: number;
    clear: number;
}

interface IActionsB {
    update: number;
    clear: number;
}

interface INamespaceMap {
    'moduleA/': IActionsA;
    'moduleA/moduleB/': IActionsB;
}

// No matter how to write it
type ConnectKey<N, T> = N + [ P in keyof T ];


interface Dispatch<Map> {
    <N extends keyof Map, K extends keyof Map[N]>(type: ConnectKey<N, K>, value: Map[N][K]): void;
}

let dispatch!: Dispatch<INamespaceMap>;
dispatch('moduleA/update', 0);
dispatch('moduleA/moduleB/delete', 0);

This will bring great help to vuex.

@anurbol
Copy link

anurbol commented Sep 14, 2018

I am writing a library that converts some values to observables:

inteface Foo {
  bar: any
}

// the library should convert above interface to 

inteface Foo2 {
  bar$: any
}

// syntax like below would be cool
type Foo2 = {
    [(K in keyof Foo) + '$']: Observable<Foo[K]>
}

// such expressions would be useful not only to mutate property names, 
// but other strings in type system as well (this behavior is not desired as much, though):
type Foo = 'bar'
type Foo2 = Foo + 'Addition' // gives the string type "barAddition"

@vitalets
Copy link

Another case.
In react-native-extended-stylesheet styles can hold media queries starting with @media ... string. For example:

{
   '@media ios': { ... },
   '@media (min-width: 350) and (max-width: 500)': { ... }
}

Currently I need to manually enumerate all keys used in app:

type ExtraStyles = {
    '@media ios'?: ExtendedStyle;
    '@media (min-width: 350) and (max-width: 500)'?: ExtendedStyle;
    ...
}

It would be great to define universal type with keys matching regexp:

type MediaQueryStyles = {
    [/@media .+/i]?: ExtendedStyle;
}

@aleclarson
Copy link

aleclarson commented Sep 24, 2018

This would help a lot with supporting dot-syntax keys à la lodash.get, I think.

Having Split<T extends string> and Join<T extends string[]> types would help, too.

@evelant
Copy link

evelant commented Sep 26, 2018

Supporting dot syntax keys would be awesome. For example MongoDB queries are frequently written as {"obj.nested.key": "value"} which is impossible to type right now.

@pedro-pedrosa
Copy link

Yet another case:

I was wondering what could be done in relation to pnp/pnpjs#199 in pnpjs. With pnpjs you can query collections of items in SharePoint and project only a subset of the properties of those items that you will need using the function select. For example:

documents.select('Id', 'Title', 'CreatedBy').get();

The library also supports selecting expanded properties of complex types. In the above example, we would be able to get both the Id and UserName subproperties of the CreatedBy property:

documents.select('Id', 'Title', 'CreatedBy/Id', 'CreatedBy/UserName').get();

It is currently impossible to type the select arguments or its return type. The library currently casts everything to any by default which is a shame since almost all usage scenarios of this library use these operations.

@evelant
Copy link

evelant commented Dec 11, 2018

Any more info on if this will be a possibility? Dot notation mongo queries are one of the last things missing typing in my project =)

@pke
Copy link

pke commented Jun 10, 2020

You can cast?:

type StyleType = key of styles

const styles = {
  base: {},
  primary: {},
  seconary: {},
  primaryHover: {},
}

const MyButton = ({type} : {type: string}]) => (
  // ...
  return <Button style={
    ...styles.base,
    ...styles[props.type],
    hover && ...styles[type + "Hover" as StyleType],
  }/>
)

@zaverden
Copy link

every time you use as you say to typescript: I can't prove you that type is correct, but "Trust Me, I'm an Engineer ©".

I'd like to have an ability to express my intentions with a language syntax and don't use as in any case.

@pke
Copy link

pke commented Jun 11, 2020

I know, it was a lame attempt to solve the problem.

@apples
Copy link

apples commented Jun 15, 2020

I've run into a use case where I need this. I have a situation where I need to transform keys of an object with just a simple concatenation. Turn ${keyof T} into ${keyof T}_changed.

I need this for almost the exact same reason. Adding "meta" fields corresponding to existing fields in an object. Our current code works by using unsafe casts.

I would love to be able to do something like this:

interface WithMeta<T extends {}> {
    [K in keyof T]: T[K];
    [K+'_meta' for K in keyof T]: MetaInfo;
}

@anilanar
Copy link
Contributor

@sk- Although I don’t prefer object oriented APIs over pure functional ones, TS is currently closer to OOP languages feature-wise. So for best practices, I would look at other OOP sound languages, without taking any JS/dynamism specific shortcuts.

JS libs had always been conformist to the idea that a library must have an easy to understand README.md; that one function should be able to handle everything with tens of overloads if not hundreds (e.g. moment.js).

Encoding valuable information in strings, I accept, was another pattern JS libs have been practicing; but I think those practices must be abolished simply because even if TS supports typelevel string manipulations, interop with other communities and languages will be terrible.

Syntax fetishism to the degree of encoding information in strings was never a good idea (typewise) and will never be a good idea until it’s proven to be a practical approach by multiple languages. Can TS take that risk by itself?

Did someone do the theoretical background work on typelevel string operations and how they would work with polymorphism? Would that require backtracking in the type resolver? Does TS type resolver support backtracking?

As strings are (in theory) list of chars, can we somehow reuse type machinery that we already have for arrays/tuples with strings?

I know that everybody watching this thread are trying to solve their real world problems for their real world projects, but those projects will be around for couple for years maybe. The language will potentially stay around longer, keep that in mind.

@MeirionHughes
Copy link
Author

MeirionHughes commented Jun 16, 2020

@anilanar

Would that require backtracking in the type resolver? Does TS type resolver support backtracking?

I think it does - TS has just changed their internal mapping (in 3.9) to:

/* @internal */
const enum TypeMapKind {
  Simple,
  Array,
  Function,
  Composite,
  Merged,
}

/* @internal */
type TypeMapper =
  | { kind: TypeMapKind.Simple, source: ts.Type, target: ts.Type }
  | { kind: TypeMapKind.Array, sources: readonly ts.Type[], targets: readonly ts.Type[] | undefined }
  | { kind: TypeMapKind.Function, func: (t: ts.Type) => ts.Type }
  | { kind: TypeMapKind.Composite | TypeMapKind.Merged, mapper1: TypeMapper, mapper2: TypeMapper };

It used to be a simple function to map A->B, but it looks like it now structures the mapping in a far more verbose and structured way. Its possible this work will make doing mutations easier, as the mappings are recursive. At first glance these should be "invertable" if the reverse B->A is needed.

If the TS team exposed an ability to manipulate the bindings/mappings and/or "rebind" during a transformation (plugins) its possible the this issue (Augmenting the Keys) could be done in Userland.

@1nVitr0
Copy link

1nVitr0 commented Jun 19, 2020

There seem to be an awful lot of issues being marked as a duplicate of this issue. Many of them propose a feature I would call "Concatenated Types" which is, in my opinion, not at all covered by this proposal.

The title of this issue suggests that this feature should only be enabled during type mapping, which would make many "Concatenated Types" impossible. For example, without going to the much more complicated regex types, simple types like this would be incredibly helpful:

// Helper Types
type Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F";
type Numeric = "0" | "1" | "2" | "3" | "4" | "5" |  "6" | "7" | "8" | "9";

// Concatenated Types
type Color = "#" + Hex + Hex + Hex + Hex + Hex + Hex | "#" + Hex + Hex + Hex;
type Email = string + "@" + string + "." + string;
type ZipCode = Numeric + Numeric + Numeric + Numeric + Numeric;

If you'd want to go really crazy, a repeat operator could also be helpful, shortening the Color example to

type Color = "#" + Repeat<Hex, 6> | "#" + Repeat<Hex, 3>

or something similar (I realize that breaks the usual Standard for Utility Types, as "6" is not strictly a type, so just sticking to manually repeating is fine). Another repeat operator could be

type ZipCode = Numeric * 5

which could offer an "infinite repeat" option for something like

type FloatString = Numeric + "." + Numeric * *

but it would be perfectly reasonable to reserve the * operator for some other bright idea in the future. Of course the * could also be used in the Repeat<> thingymajig making the second argument even less type-like.

This would also change the way this feature is used in mapped types. The original example would then more likely become something like this:

type Changed<T> = {          
   [ P in (keyof T) + "Changed"]?: Function;
}

With the addition of optional parts, Concatented Types could become even more useful for purposes like

// Simple concatenated type
type RgbValue = Numeric * 2 | "1" + Numeric * 2 | "2" + ("0" | "1" | "2" | "3" | "4") + Numeric | "25" + ("0" | "1" | "2" | "3" | "4" | "5");
// Concatenated types with optional type
type AlphaValue = "0" | "1" | "0"? + "." + Numeric * *;
type Color = "#" + Hex * 6 | "#" + Hex * 3 | "rgba(" + (RgbValue + ", ") * 2 + RgbValue + (", " + AlphaValue)? + ")";

which is not the most beautiful thing, but it would make stuff like this at least possible without writing 16.5 Million Cases.

All this would of course only sense on string types, just like it is the case with the original proposal. Something like type Weight = number + "kg" would be nice, but I don't see how the type safety would work as any number + string is simply a string, and has strictly speaking nothing to do with a number anymore.

@MeirionHughes
Copy link
Author

MeirionHughes commented Jun 19, 2020

Personally I think what you're describing is blurring the line between structural type and value/formatting. Ultimately TS is to define structure. While TS does blur the line a little with strings, it only does so in-so-far as the strings boil down to a constant.

The "structure" in your examples could perfectly be defined (and works now) via Tuples.

type Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F";
type Numeric = "0" | "1" | "2" | "3" | "4" | "5" |  "6" | "7" | "8" | "9";

type RGB = ["#", Hex, Hex, Hex, Hex, Hex, Hex];
type RGBA = ["#", Hex, Hex, Hex, Hex, Hex, Hex, Hex, Hex];

type Color = RGB | RGBA;
type Email = [string, "@", string, ".", string];
type ZipCode = [Numeric, Numeric, Numeric, Numeric, Numeric];

there is also some support for repeating, with tuple rest operator:

type FloatString  = [Numeric, '.', ...Numeric[]];

Although it does not currently let you do the following (others trying to solve it):

type X3<T> = [T, T, T];
type X6<T> = [T, T, T, T, T, T];

type Color2 = ["#", ...X6<Hex>] | ["#", ...X3<Hex>] // error

In my opinion you would be better served by asking for typescript to support strings as an ArrayLike such that:

let a: FloatString = "1.23"; 

is valid - that way, even though you're working with strings - they can be matched against tuple types. i.e. for any string "1.23" it is implicitly treated as a Tuple ["1", ".", "2", "3"] during evaluation against another Tuple type.

@1nVitr0
Copy link

1nVitr0 commented Jun 20, 2020

Supporting strings as an ArrayLike seems like a great idea. Tuples alone for this case is more of a hack than anything else. ['#' ,'0', '1', '2', '3', '4', '5'] is less readable than simply defining an RGB tuple or class and then transforming it into a CSS string.

It's perfectly reasonable to not support concatenating string types due to the problems you described. Nevertheless I think it would be a great feature and thought I should mention it here, since most of the issues related to it are marked as duplicate of this issue. Regex types are the alternative, and in my opinion they go too far and open the door to performance degradation.

@ldd
Copy link

ldd commented Jun 22, 2020

Note: I stumbled upon this problem and I want a proper solution too.

In the meantime, for people that are having problems with strings, I found an alternative way of thinking about this with type guards for my problem:

type pet = "dog" | "cat" | "default";
const intensity: Record<pet, number> = { dog: 5, cat: 9, default: 12 };

const isPet = (s: string): s is pet => intensity[s as pet] !== undefined;

function getIntensityFromLabel(label: string): number {
  if (isPet(label)) return intensity[label];

  const [reversedDirectionLabel] = label.split("Reversed");
  if (isPet(reversedDirectionLabel))
    return intensity[reversedDirectionLabel] * -1;
  return intensity.default;
}

getIntensityFromLabel("dogReversed"); // -5
getIntensityFromLabel("dog"); // 5
getIntensityFromLabel("iLoveCake"); // 12

The code should be self-explanatory, but the point is that in my function, which would need an augmented type, I use a more loose type (string), and then use type guards to get what I want.

Keep in mind that this approach absolutely does not solve the issue, but for some use cases, type guards are good friends.

@Harpush
Copy link

Harpush commented Jul 18, 2020

I think the minimum required feature here which seems to me pretty reasonable is literal string type concatenation. I believe any other literal type manipulation is far too complex.
So generally just:

type a = 'hhh';
type b = 'ggg';
type ab = a + b; // hhhggg

Of course with generic support 😄

@pke
Copy link

pke commented Jul 19, 2020

type Email = [string, "@", string, ".", string];

I'd be fine with that, it it would work:

email = "test@gmail.com"
Type '"test@gmail.com"' is not assignable to type '[string, "@", string, ".", string]'.ts(2322)

@shuaihuGao
Copy link

When will this feature be added?, it's too necessary for me.

@brian428
Copy link

I'd also love to see something like this. Right now TypeScript is unable to cope with any code that generates functions at runtime with "template-like" dynamic function names. E.g. code that takes a label/key such as "Users" and creates functions for fetchUsers(), selectUsers(), selectIsUsersLoading(), etc.

Currently, the only possible way to handle it is to painstakingly define interfaces for every dynamic method, for every varying label/key. Which is obviously infeasible.

@Retsam
Copy link

Retsam commented Jul 22, 2020

The workaround for that case is to have the API be like api.users.fetch(), instead of api.fetchUsers(). That's reasonably supported by existing TS machinery, and I find is cleaner in implementation, anyway. (I know not all APIs are able/willing to make those sort of breaking changes)

@brian428
Copy link

@Retsam right, obviously that would be the preferred option. But in cases where you can't make that breaking change or don't have control of a library that works this way, changing the API isn't an option.

@ahejlsberg
Copy link
Member

This suggestion is now implemented in #40336.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined Domain: Mapped Types The issue relates to mapped types In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.