├── .DS_Store ├── out ├── app.d.ts ├── samples.d.ts ├── app.d.ts.map ├── app.js.map ├── samples.d.ts.map ├── app.js ├── samples.js ├── samples.js.map ├── lenses.js.map ├── lenses.js ├── lenses.d.ts.map └── lenses.d.ts ├── media ├── TypeSafetyOfNesting.gif └── TypeSafetyOfRename.gif ├── app.ts ├── yarn.lock ├── package.json ├── .vscode └── launch.json ├── LICENSE ├── samples.ts ├── lenses.ts ├── tsconfig.json ├── readme.md └── .gitignore /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoppinger/ts-lenses/HEAD/.DS_Store -------------------------------------------------------------------------------- /out/app.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./lenses"; 2 | //# sourceMappingURL=app.d.ts.map -------------------------------------------------------------------------------- /out/samples.d.ts: -------------------------------------------------------------------------------- 1 | export declare const run: () => void; 2 | //# sourceMappingURL=samples.d.ts.map -------------------------------------------------------------------------------- /media/TypeSafetyOfNesting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoppinger/ts-lenses/HEAD/media/TypeSafetyOfNesting.gif -------------------------------------------------------------------------------- /media/TypeSafetyOfRename.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoppinger/ts-lenses/HEAD/media/TypeSafetyOfRename.gif -------------------------------------------------------------------------------- /out/app.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../app.ts"],"names":[],"mappings":"AAGA,cAAc,UAAU,CAAA"} -------------------------------------------------------------------------------- /out/app.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.js","sourceRoot":"","sources":["../app.ts"],"names":[],"mappings":";;;;;AAGA,8BAAwB;AAExB,QAAQ;AAER;;;;EAIE"} -------------------------------------------------------------------------------- /out/samples.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"samples.d.ts","sourceRoot":"","sources":["../samples.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,GAAG,YAiEf,CAAA"} -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "./lenses" 2 | import { run } from "./samples" 3 | 4 | export * from "./lenses" 5 | 6 | // run() 7 | 8 | /* TODO: 9 | * readme 10 | * npm package 11 | * article 12 | */ 13 | -------------------------------------------------------------------------------- /out/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./lenses")); 7 | // run() 8 | /* TODO: 9 | * readme 10 | * npm package 11 | * article 12 | */ 13 | //# sourceMappingURL=app.js.map -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | typescript@^3.8.3: 6 | version "3.8.3" 7 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" 8 | integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-lenses", 3 | "version": "0.9.4", 4 | "description": "A statically typed lens library for TypeScript", 5 | "main": "./out/app.js", 6 | "scripts": { 7 | "watch": "tsc --build --watch", 8 | "go": "node ./out/app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/hoppinger/ts-lenses.git" 13 | }, 14 | "author": "Dr. Giuseppe Maggiore", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/hoppinger/ts-lenses/issues" 18 | }, 19 | "homepage": "https://github.com/hoppinger/ts-lenses#readme", 20 | "dependencies": { 21 | "typescript": "^3.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/out/app.js", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hoppinger BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /out/samples.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const lenses_1 = require("./lenses"); 4 | exports.run = () => { 5 | const p1 = lenses_1.Entity({ name: "John", surname: "Doe", age: 27 }); 6 | const q1 = p1.set("age", a => a + 1).set("name", _ => "Jane").commit(); 7 | const q2 = p1.set("age", a => a + 1).commit(); 8 | const q3 = p1.rename("age", "birthday", x => new Date("1-1-2001")).commit(); 9 | const p2 = lenses_1.Entity({ nesting1: { nesting2: { nesting3: { nesting4: { slightlyLessObscenelyNestedValueWeNeedToUpdate: 0, nesting5: { obscenelyNestedValueWeNeedToUpdate: 0 } } } } } }); 10 | const q21 = p2.setIn("nesting1", e => e.setIn("nesting2", e => e.setIn("nesting3", e => e.setIn("nesting4", e => e.set("slightlyLessObscenelyNestedValueWeNeedToUpdate", v => v + 2) 11 | .rename("slightlyLessObscenelyNestedValueWeNeedToUpdate", "counter", x => x) 12 | .setIn("nesting5", e => e.set("obscenelyNestedValueWeNeedToUpdate", v => v + 1) 13 | .rename("obscenelyNestedValueWeNeedToUpdate", "counter", x => x)))))).commit(); 14 | const setUserName = (newUserName) => (s0) => lenses_1.Entity(s0) 15 | .setIn("loginForm", e => e 16 | .setIn("firstPage", e => e 17 | .set("userName", _ => newUserName))) 18 | .commit(); 19 | console.log("Done"); 20 | }; 21 | //# sourceMappingURL=samples.js.map -------------------------------------------------------------------------------- /out/samples.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"samples.js","sourceRoot":"","sources":["../samples.ts"],"names":[],"mappings":";;AAAA,qCAAiC;AAEpB,QAAA,GAAG,GAAG,GAAG,EAAE;IAOtB,MAAM,EAAE,GAAG,eAAM,CAAS,EAAE,IAAI,EAAC,MAAM,EAAE,OAAO,EAAC,KAAK,EAAE,GAAG,EAAC,EAAE,EAAE,CAAC,CAAA;IACjE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAA;IACpE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;IAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,EAAE,CAAA;IAiB3E,MAAM,EAAE,GAAG,eAAM,CAAc,EAAE,QAAQ,EAAC,EAAE,QAAQ,EAAC,EAAE,QAAQ,EAAC,EAAE,QAAQ,EAAC,EAAE,8CAA8C,EAAC,CAAC,EAAE,QAAQ,EAAC,EAAE,kCAAkC,EAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;IAC3L,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CACnC,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CACtB,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CACxB,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CACtB,CAAC,CAAC,GAAG,CAAC,gDAAgD,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;SAChE,MAAM,CAAC,gDAAgD,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;SAC3E,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CACrB,CAAC,CAAC,GAAG,CAAC,oCAAoC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,GAAC,CAAC,CAAC;SACnD,MAAM,CAAC,oCAAoC,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAClE,CACF,CACF,CACF,CACF,CAAC,MAAM,EAAE,CAAA;IAgBV,MAAM,WAAW,GAAG,CAAC,WAAkB,EAAE,EAAE,CAAC,CAAC,EAAW,EAAa,EAAE,CACrE,eAAM,CAAC,EAAE,CAAC;SACP,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;SACzB,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;SACzB,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;SACnC,MAAM,EAAE,CAAA;IAEb,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACrB,CAAC,CAAA"} -------------------------------------------------------------------------------- /samples.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "./lenses" 2 | 3 | export const run = () => { 4 | interface Person { 5 | name:string, 6 | surname:string, 7 | age:number 8 | } 9 | 10 | const p1 = Entity({ name:"John", surname:"Doe", age:27 }) 11 | const q1 = p1.set("age", a => a+1).set("name", _ => "Jane").commit() 12 | const q2 = p1.set("age", a => a+1).commit() 13 | const q3 = p1.rename("age", "birthday", x => new Date("1-1-2001")).commit() 14 | 15 | interface NestedState { 16 | nesting1:{ 17 | nesting2:{ 18 | nesting3:{ 19 | nesting4:{ 20 | nesting5:{ 21 | obscenelyNestedValueWeNeedToUpdate:number 22 | }, 23 | slightlyLessObscenelyNestedValueWeNeedToUpdate:number 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | const p2 = Entity({ nesting1:{ nesting2:{ nesting3:{ nesting4:{ slightlyLessObscenelyNestedValueWeNeedToUpdate:0, nesting5:{ obscenelyNestedValueWeNeedToUpdate:0 } } } } } }) 31 | const q21 = p2.setIn("nesting1", e => 32 | e.setIn("nesting2", e => 33 | e.setIn("nesting3", e => 34 | e.setIn("nesting4", e => 35 | e.set("slightlyLessObscenelyNestedValueWeNeedToUpdate", v => v + 2) 36 | .rename("slightlyLessObscenelyNestedValueWeNeedToUpdate", "counter", x => x) 37 | .setIn("nesting5", e => 38 | e.set("obscenelyNestedValueWeNeedToUpdate", v => v+1) 39 | .rename("obscenelyNestedValueWeNeedToUpdate", "counter", x => x) 40 | ) 41 | ) 42 | ) 43 | ) 44 | ).commit() 45 | 46 | 47 | interface AppState { 48 | loginForm:{ 49 | firstPage:{ 50 | userName:string 51 | password:string 52 | }, 53 | secondPage:{ 54 | email:string 55 | accountType:number 56 | } 57 | } 58 | } 59 | 60 | const setUserName = (newUserName:string) => (s0:AppState) : AppState => 61 | Entity(s0) 62 | .setIn("loginForm", e => e 63 | .setIn("firstPage", e => e 64 | .set("userName", _ => newUserName))) 65 | .commit() 66 | 67 | console.log("Done") 68 | } 69 | -------------------------------------------------------------------------------- /out/lenses.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"lenses.js","sourceRoot":"","sources":["../lenses.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /out/lenses.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __rest = (this && this.__rest) || function (s, e) { 3 | var t = {}; 4 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 5 | t[p] = s[p]; 6 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 7 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 8 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 9 | t[p[i]] = s[p[i]]; 10 | } 11 | return t; 12 | }; 13 | Object.defineProperty(exports, "__esModule", { value: true }); 14 | exports.Without = (key, _a) => { 15 | var _b = key, _ = _a[_b], values = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]); 16 | return values; 17 | }; 18 | exports.Rename = (keyOld, keyNew, _a) => { 19 | var _b = keyOld, value = _a[_b], values = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]); 20 | return (Object.assign(Object.assign({}, values), { [keyNew]: value })); 21 | }; 22 | exports.Entity = (fields) => ({ 23 | get: (...keys) => keys.reduce((acc, k) => (Object.assign(Object.assign({}, acc), fields[k])), {}), 24 | nested: exports.NestedEntity(fields), 25 | inline: exports.InlineEntity(fields), 26 | rename: (key, newKey, f) => exports.Entity(exports.Rename(key, newKey, Object.assign(Object.assign({}, fields), { [key]: f(fields[key]) }))), 27 | set: function (key, f) { 28 | return this.inline.lazy(key, f); 29 | }, 30 | setIn: function (key, f) { 31 | return this.nested.lazy(key, x => f(x).commit()); 32 | }, 33 | commit: () => fields, 34 | }); 35 | exports.NestedEntity = (fields) => ({ 36 | eager: (key, f) => (Object.assign(Object.assign({}, fields), { [key]: f(exports.Entity(fields[key])) })), 37 | lazy: (key, f) => exports.Entity(Object.assign(Object.assign({}, fields), { [key]: f(exports.Entity(fields[key])) })) 38 | }); 39 | exports.InlineEntity = (fields) => ({ 40 | eager: (key, f) => (Object.assign(Object.assign({}, fields), { [key]: f(fields[key]) })), 41 | lazy: (key, f) => exports.Entity(Object.assign(Object.assign({}, fields), { [key]: f(fields[key]) })) 42 | }); 43 | exports.setter = (k, v) => (s) => exports.Entity(s).inline.eager(k, _ => v); 44 | exports.Updater = () => (f) => fields => f(exports.Entity(fields)); 45 | //# sourceMappingURL=lenses.js.map -------------------------------------------------------------------------------- /out/lenses.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"lenses.d.ts","sourceRoot":"","sources":["../lenses.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /out/lenses.d.ts: -------------------------------------------------------------------------------- 1 | declare type Fun = (_: a) => b; 2 | declare type Without = Pick>; 3 | export declare const Without: (key: K, { [key]: _, ...values }: T) => Pick>; 4 | export declare const Rename: (keyOld: KOld, keyNew: KNew, { [keyOld]: value, ...values }: T) => Pick> & { [x in KNew]: T[KOld]; }; 5 | export declare type SetField = { 6 | [x in Exclude]: fields[x]; 7 | } & { 8 | [x in k]: v; 9 | }; 10 | export declare type RenameField = Without & { 11 | [x in newK]: fields[k]; 12 | }; 13 | export declare type UpdateSort = "nested-eager" | "nested-lazy" | "inline-eager" | "inline-lazy"; 14 | export declare type UpdateInput = u extends "nested-eager" ? Entity : u extends "nested-lazy" ? Entity : u extends "inline-eager" ? field : u extends "inline-lazy" ? field : never; 15 | export declare type UpdateOutput = u extends "nested-eager" ? field : u extends "inline-eager" ? field : u extends "nested-lazy" ? Entity : u extends "inline-lazy" ? Entity : never; 16 | export interface Entity { 17 | get: (...keys: k[]) => Pick; 18 | rename: (key: k, newKey: newK, f: Fun) => Entity, newK, v>>; 19 | set: (key: k, f: Fun, v>) => UpdateOutput<"inline-lazy", SetField>; 20 | setIn: (key: k, f: Fun, Entity>) => UpdateOutput<"nested-lazy", SetField>; 21 | nested: NestedEntity; 22 | inline: InlineEntity; 23 | commit: () => fields; 24 | } 25 | export interface NestedEntity { 26 | eager: (key: k, f: Fun, v>) => UpdateOutput<"nested-eager", SetField>; 27 | lazy: (key: k, f: Fun, v>) => UpdateOutput<"nested-lazy", SetField>; 28 | } 29 | export interface InlineEntity { 30 | eager: (key: k, f: Fun, v>) => UpdateOutput<"inline-eager", SetField>; 31 | lazy: (key: k, f: Fun, v>) => UpdateOutput<"inline-lazy", SetField>; 32 | } 33 | export declare const Entity: (fields: fields) => Entity; 34 | export declare const NestedEntity: (fields: fields) => NestedEntity; 35 | export declare const InlineEntity: (fields: fields) => InlineEntity; 36 | export declare const setter: (k: k, v: fields[k]) => (s: fields) => SetField; 37 | export declare const Updater: () => (f: Fun, result>) => Fun; 38 | export {}; 39 | //# sourceMappingURL=lenses.d.ts.map -------------------------------------------------------------------------------- /lenses.ts: -------------------------------------------------------------------------------- 1 | type Fun = (_:a) => b 2 | type Without = Pick> 3 | 4 | export const Without = (key: K, { [key]: _, ...values }: T): Without => values 5 | export const Rename = (keyOld: KOld, keyNew: KNew, { [keyOld]: value, ...values }: T): 6 | Without & { [x in KNew]: T[KOld] } => 7 | ({ ...values, ...{ [keyNew]: value } as { [x in KNew]: T[KOld] } }) 8 | 9 | export type SetField = { [x in Exclude]:fields[x] } & { [x in k]:v } 10 | export type RenameField = Without & { [x in newK]: fields[k] } 11 | 12 | export type UpdateSort = "nested-eager" | "nested-lazy" | "inline-eager" | "inline-lazy" 13 | export type UpdateInput = 14 | u extends "nested-eager" ? Entity : 15 | u extends "nested-lazy" ? Entity : 16 | u extends "inline-eager" ? field : 17 | u extends "inline-lazy" ? field : 18 | never 19 | export type UpdateOutput = 20 | u extends "nested-eager" ? field : 21 | u extends "inline-eager" ? field : 22 | u extends "nested-lazy" ? Entity : 23 | u extends "inline-lazy" ? Entity : 24 | never 25 | 26 | export interface Entity { 27 | get: (...keys:k[]) => Pick 28 | rename: (key:k, newKey:newK, f:Fun) => Entity, newK, v>> 29 | set: (key:k, f:Fun, v>) => UpdateOutput<"inline-lazy", SetField> 30 | setIn: (key:k, f:Fun, Entity>) => UpdateOutput<"nested-lazy", SetField> 31 | nested:NestedEntity, 32 | inline:InlineEntity, 33 | commit:() => fields 34 | } 35 | 36 | export interface NestedEntity { 37 | eager: (key:k, f:Fun, v>) => UpdateOutput<"nested-eager", SetField>, 38 | lazy: (key:k, f:Fun, v>) => UpdateOutput<"nested-lazy", SetField> 39 | } 40 | 41 | export interface InlineEntity { 42 | eager: (key:k, f:Fun, v>) => UpdateOutput<"inline-eager", SetField>, 43 | lazy: (key:k, f:Fun, v>) => UpdateOutput<"inline-lazy", SetField> 44 | } 45 | 46 | export const Entity = (fields:fields) : Entity => ({ 47 | get: (...keys:k[]) : Pick => 48 | keys.reduce((acc, k) => ({...acc, ...fields[k] }), {} as Pick), 49 | nested:NestedEntity(fields), 50 | inline:InlineEntity(fields), 51 | rename: (key:k, newKey:newK, f:Fun) : 52 | Entity, newK, v>> => 53 | Entity(Rename(key, newKey, {...fields, [key]:f(fields[key])})) as any, 54 | set : function(this:Entity, key:k, f:Fun, v>) 55 | : UpdateOutput<"inline-lazy", SetField> { 56 | return this.inline.lazy(key, f) 57 | }, 58 | setIn: function(this:Entity, key:k, f:Fun, Entity>) 59 | : UpdateOutput<"nested-lazy", SetField> { 60 | return this.nested.lazy(key, x => f(x).commit()) 61 | }, 62 | commit:() : fields => fields, 63 | }) 64 | 65 | export const NestedEntity = (fields:fields) : NestedEntity => ({ 66 | eager: (key:k, f:Fun, v>) 67 | : UpdateOutput<"nested-eager", SetField> => 68 | ({...fields, [key]:f(Entity(fields[key]))}), 69 | lazy: (key:k, f:Fun, v>) 70 | : UpdateOutput<"nested-lazy", SetField> => 71 | Entity({...fields, [key]:f(Entity(fields[key]))}) 72 | }) 73 | export const InlineEntity = (fields:fields) : InlineEntity => ({ 74 | eager: (key:k, f:Fun, v>) 75 | : UpdateOutput<"inline-eager", SetField> => 76 | ({...fields, [key]:f(fields[key])}), 77 | lazy: (key:k, f:Fun, v>) 78 | : UpdateOutput<"inline-lazy", SetField> => 79 | Entity({...fields, [key]:f(fields[key])}) 80 | }) 81 | 82 | export const setter = (k:k, v:fields[k]) => 83 | (s:fields) => Entity(s).inline.eager(k, _ => v) 84 | 85 | export const Updater = () => (f:Fun, result>) : Fun => 86 | fields => f(Entity(fields)) 87 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./out", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "exclude": [ 67 | "node_modules", 68 | "out" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Type\-safe lenses in TypeScript 2 | _By Dr. Giuseppe Maggiore_ 3 | 4 | Modern Single Page Applications (SPA's) built in TypeScript and JavaScript need to manage more and more complex and nested data structures. For example, we could need to manage a state such as the following\: 5 | 6 | ```ts 7 | interface AppState { 8 | loginForm:{ 9 | firstPage:{ 10 | userName:string 11 | password:string 12 | }, 13 | secondPage:{ 14 | email:string 15 | accountType:number 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | Whenever we have to _immutably_ update the `userName` as a result of a user action, we might end up writing code looking like the following\: 22 | 23 | ```ts 24 | const setUserName = (newUserName:string) => (s0:AppState) : AppState => ({ 25 | ...s0, 26 | loginForm:{ 27 | ...s0.loginForm, 28 | firstPage:{ 29 | ...s0.loginForm.firstPage, 30 | userName:newUserName 31 | } 32 | } 33 | }) 34 | ``` 35 | 36 | While it is, I believe, pretty awesome that TypeScript offers us a type\-safe spread operator, the nesting is a bit painful to watch. The fact that we cannot take advantage of the implicit context that we are copying `s0`, and as such we want to stick to it when updating its nested objects such as `loginForm` and `firstPage` requires repetition, which increases cognitive load when maintaining the code, and is error\-prone (one might mistakenly write `s1.loginForm.firstPage` if the scope contains another state, which sometimes is the case, without the compiler being able to offer any helpful warnings). 37 | 38 | In order to tackle this problem, I have built a simple lenses library that takes on the task of performing updates on TypeScript records in a way that is type\-safe, and as contextually smart as possible. 39 | 40 | 41 | ## Simple example 42 | > In order to run this example, please first install the package via `npm install ts-lenses`, and add `import { Entity } from "ts-lenses"` to the top of your file! 43 | 44 | Consider a simple, shallow type such as\: 45 | 46 | ```ts 47 | interface Person { 48 | name:string, 49 | surname:string, 50 | age:number 51 | } 52 | ``` 53 | 54 | We can wrap an object of type `Person` into a lazy `Entity` that can be updated with some smarter operators\: 55 | 56 | ```ts 57 | const p1 = Entity({ name:"John", surname:"Doe", age:27 }) 58 | ``` 59 | 60 | We can now set values as follows\: 61 | 62 | ```ts 63 | const q1 = p1.set("age", a => a+1).set("name", _ => "Jane") 64 | ``` 65 | 66 | Notice that, in order to enable method chaining, the `set` operator does not return the final result, but rather a new `Entity` on which further operations can be performed. Of course, `set` is type\-safe\: `"age"` must be a valid attribute, and the setter function that updates the value must process an input and produce an output of the correct type. 67 | 68 | When we are done with chaining operations, we can commit and then we get the resulting object with the values set correctly\: 69 | 70 | ```ts 71 | const q1 = p1.set("age", a => a+1).set("name", _ => "Jane").commit() 72 | ``` 73 | 74 | We can also do some nice things like change the structure (and thus the type) of the resulting record\: 75 | 76 | ```ts 77 | const q2 = p1.rename("age", "birthday", x => new Date("1-1-2001")).commit() 78 | ``` 79 | 80 | The type of `q2` now has no `age` attribute anymore, and instead has a `birthday` of type `Date`\: 81 | 82 | ```ts 83 | q2 : { 84 | name:string, 85 | surname:string, 86 | birthday:Date 87 | } 88 | ``` 89 | 90 | This means that further operator chaining after a rename cannot access the old attribute, but rather only the new\: 91 | 92 | ![IntelliSense after rename](./media/TypeSafetyOfRename.gif) 93 | 94 | ## More complex example 95 | We can also work on nested objects. For example, consider a fictitious type such as\: 96 | 97 | ```ts 98 | interface NestedState { 99 | nesting1:{ 100 | nesting2:{ 101 | nesting3:{ 102 | nesting4:{ 103 | nesting5:{ 104 | obscenelyNestedValueWeNeedToUpdate:number 105 | }, 106 | slightlyLessObscenelyNestedValueWeNeedToUpdate:number 107 | } 108 | } 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | Imagine that we were tasked with incrementing both nested numbers by `1`. A bit of a daunting task, especially if we consider the amount of repetition involved. Writing something like `{...s0.nesting1.nesting2.nesting3.nesting4.nesting5, obscenelyNestedValueWeNeedToUpdate:s0.nesting1.nesting2.nesting3.nesting4.nesting5.obscenelyNestedValueWeNeedToUpdate+1}` is not exactly that paragon of elegance that gives most developers that feeling of "yes! I love my job" :) 115 | 116 | The library can help us a bit. The `setIn` operator facilitates setting nested values. The resulting code would look as follows\: 117 | 118 | ```ts 119 | const p2 = Entity({ nesting1:{ nesting2:{ nesting3:{ nesting4:{ slightlyLessObscenelyNestedValueWeNeedToUpdate:0, nesting5:{ obscenelyNestedValueWeNeedToUpdate:0 } } } } } }) 120 | const q21 = p2.setIn("nesting1", e => 121 | e.setIn("nesting2", e => 122 | e.setIn("nesting3", e => 123 | e.setIn("nesting4", e => 124 | e.set("slightlyLessObscenelyNestedValueWeNeedToUpdate", v => v + 2) 125 | .setIn("nesting5", e => 126 | e.set("obscenelyNestedValueWeNeedToUpdate", v => v+1) 127 | ) 128 | ) 129 | ) 130 | ) 131 | ).commit() 132 | ``` 133 | 134 | Of course, we enjoy type\-safety all the way down, and we can mix and match `set` and `setIn` as needed. We could even rename some of the nested attributes, in order to both update and restructure the input state for update\-and\-convert tasks: 135 | 136 | ![IntelliSense, nesting, and renaming](./media/TypeSafetyOfNesting.gif) 137 | 138 | 139 | ## The original issue 140 | The original "challenging" bit of code then becomes\: 141 | 142 | ```ts 143 | const setUserName = (newUserName:string) => (s0:AppState) : AppState => 144 | Entity(s0) 145 | .setIn("loginForm", e => e 146 | .setIn("firstPage", e => e 147 | .set("userName", _ => newUserName))) 148 | .commit() 149 | ``` 150 | 151 | Of course, it is a matter of personal preference, but I find this much more attractive than the original version! 152 | 153 | # Conclusion 154 | Managing immutable update operations on complex nested states is a recurring challenge. In this article I present a small, new library that wraps these operations in a type\-safe way, inspired from the _lenses_ concept from Haskell. 155 | 156 | Thanks to this library, [which can be found on `npm`](https://www.npmjs.com/package/ts-lenses), you can process data quickly and easily, with enhanced productivity and less bugs. 157 | 158 | Thank you for coming all the way to the end, I hope you enjoyed reading this article as much as I enjoyed writing it ;) 159 | 160 | 161 | # Appendix\: about the author 162 | Hi! I am Giuseppe Maggiore. I have an academic background (PhD) in Computer Science, specifically compilers and functional programming (not so surprising eh...). I am now CTO of [Hoppinger](https://www.hoppinger.com/), a wonderful software development company in the heart of Rotterdam (Netherlands). 163 | 164 | I am always looking for talented software engineers who get excited at the thought of type safety, reliable software, functional programming, and so on. If that is the case, do get in touch with us, [we always have open positions for smart people](https://www.hoppinger.com/vacatures/)! 165 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/.yarn-integrity 2 | node_modules/.bin/tsc 3 | node_modules/.bin/tsserver 4 | node_modules/typescript/AUTHORS.md 5 | node_modules/typescript/CODE_OF_CONDUCT.md 6 | node_modules/typescript/CopyrightNotice.txt 7 | node_modules/typescript/LICENSE.txt 8 | node_modules/typescript/package.json 9 | node_modules/typescript/README.md 10 | node_modules/typescript/ThirdPartyNoticeText.txt 11 | node_modules/typescript/bin/tsc 12 | node_modules/typescript/bin/tsserver 13 | node_modules/typescript/lib/cancellationToken.js 14 | node_modules/typescript/lib/diagnosticMessages.generated.json 15 | node_modules/typescript/lib/lib.d.ts 16 | node_modules/typescript/lib/lib.dom.d.ts 17 | node_modules/typescript/lib/lib.dom.iterable.d.ts 18 | node_modules/typescript/lib/lib.es5.d.ts 19 | node_modules/typescript/lib/lib.es6.d.ts 20 | node_modules/typescript/lib/lib.es2015.collection.d.ts 21 | node_modules/typescript/lib/lib.es2015.core.d.ts 22 | node_modules/typescript/lib/lib.es2015.d.ts 23 | node_modules/typescript/lib/lib.es2015.generator.d.ts 24 | node_modules/typescript/lib/lib.es2015.iterable.d.ts 25 | node_modules/typescript/lib/lib.es2015.promise.d.ts 26 | node_modules/typescript/lib/lib.es2015.proxy.d.ts 27 | node_modules/typescript/lib/lib.es2015.reflect.d.ts 28 | node_modules/typescript/lib/lib.es2015.symbol.d.ts 29 | node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts 30 | node_modules/typescript/lib/lib.es2016.array.include.d.ts 31 | node_modules/typescript/lib/lib.es2016.d.ts 32 | node_modules/typescript/lib/lib.es2016.full.d.ts 33 | node_modules/typescript/lib/lib.es2017.d.ts 34 | node_modules/typescript/lib/lib.es2017.full.d.ts 35 | node_modules/typescript/lib/lib.es2017.intl.d.ts 36 | node_modules/typescript/lib/lib.es2017.object.d.ts 37 | node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts 38 | node_modules/typescript/lib/lib.es2017.string.d.ts 39 | node_modules/typescript/lib/lib.es2017.typedarrays.d.ts 40 | node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts 41 | node_modules/typescript/lib/lib.es2018.asynciterable.d.ts 42 | node_modules/typescript/lib/lib.es2018.d.ts 43 | node_modules/typescript/lib/lib.es2018.full.d.ts 44 | node_modules/typescript/lib/lib.es2018.intl.d.ts 45 | node_modules/typescript/lib/lib.es2018.promise.d.ts 46 | node_modules/typescript/lib/lib.es2018.regexp.d.ts 47 | node_modules/typescript/lib/lib.es2019.array.d.ts 48 | node_modules/typescript/lib/lib.es2019.d.ts 49 | node_modules/typescript/lib/lib.es2019.full.d.ts 50 | node_modules/typescript/lib/lib.es2019.object.d.ts 51 | node_modules/typescript/lib/lib.es2019.string.d.ts 52 | node_modules/typescript/lib/lib.es2019.symbol.d.ts 53 | node_modules/typescript/lib/lib.es2020.bigint.d.ts 54 | node_modules/typescript/lib/lib.es2020.d.ts 55 | node_modules/typescript/lib/lib.es2020.full.d.ts 56 | node_modules/typescript/lib/lib.es2020.promise.d.ts 57 | node_modules/typescript/lib/lib.es2020.string.d.ts 58 | node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts 59 | node_modules/typescript/lib/lib.esnext.array.d.ts 60 | node_modules/typescript/lib/lib.esnext.asynciterable.d.ts 61 | node_modules/typescript/lib/lib.esnext.bigint.d.ts 62 | node_modules/typescript/lib/lib.esnext.d.ts 63 | node_modules/typescript/lib/lib.esnext.full.d.ts 64 | node_modules/typescript/lib/lib.esnext.intl.d.ts 65 | node_modules/typescript/lib/lib.esnext.symbol.d.ts 66 | node_modules/typescript/lib/lib.scripthost.d.ts 67 | node_modules/typescript/lib/lib.webworker.d.ts 68 | node_modules/typescript/lib/lib.webworker.importscripts.d.ts 69 | node_modules/typescript/lib/protocol.d.ts 70 | node_modules/typescript/lib/README.md 71 | node_modules/typescript/lib/tsc.js 72 | node_modules/typescript/lib/tsserver.js 73 | node_modules/typescript/lib/tsserverlibrary.d.ts 74 | node_modules/typescript/lib/tsserverlibrary.js 75 | node_modules/typescript/lib/typescript.d.ts 76 | node_modules/typescript/lib/typescript.js 77 | node_modules/typescript/lib/typescriptServices.d.ts 78 | node_modules/typescript/lib/typescriptServices.js 79 | node_modules/typescript/lib/typesMap.json 80 | node_modules/typescript/lib/typingsInstaller.js 81 | node_modules/typescript/lib/watchGuard.js 82 | node_modules/typescript/lib/cs/diagnosticMessages.generated.json 83 | node_modules/typescript/lib/de/diagnosticMessages.generated.json 84 | node_modules/typescript/lib/es/diagnosticMessages.generated.json 85 | node_modules/typescript/lib/fr/diagnosticMessages.generated.json 86 | node_modules/typescript/lib/it/diagnosticMessages.generated.json 87 | node_modules/typescript/lib/ja/diagnosticMessages.generated.json 88 | node_modules/typescript/lib/ko/diagnosticMessages.generated.json 89 | node_modules/typescript/lib/pl/diagnosticMessages.generated.json 90 | node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json 91 | node_modules/typescript/lib/ru/diagnosticMessages.generated.json 92 | node_modules/typescript/lib/tr/diagnosticMessages.generated.json 93 | node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json 94 | node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json 95 | node_modules/typescript/loc/lcl/CHS/Targets/ProjectItemsSchema.xaml.lcl 96 | node_modules/typescript/loc/lcl/CHS/Targets/TypeScriptCompile.xaml.lcl 97 | node_modules/typescript/loc/lcl/CHS/Targets/TypeScriptProjectProperties.xaml.lcl 98 | node_modules/typescript/loc/lcl/CHS/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 99 | node_modules/typescript/loc/lcl/CHS/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 100 | node_modules/typescript/loc/lcl/CHS/TypeScriptTasks/TypeScript.Tasks.dll.lcl 101 | node_modules/typescript/loc/lcl/CHT/Targets/ProjectItemsSchema.xaml.lcl 102 | node_modules/typescript/loc/lcl/CHT/Targets/TypeScriptCompile.xaml.lcl 103 | node_modules/typescript/loc/lcl/CHT/Targets/TypeScriptProjectProperties.xaml.lcl 104 | node_modules/typescript/loc/lcl/CHT/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 105 | node_modules/typescript/loc/lcl/CHT/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 106 | node_modules/typescript/loc/lcl/CHT/TypeScriptTasks/TypeScript.Tasks.dll.lcl 107 | node_modules/typescript/loc/lcl/CSY/Targets/ProjectItemsSchema.xaml.lcl 108 | node_modules/typescript/loc/lcl/CSY/Targets/TypeScriptCompile.xaml.lcl 109 | node_modules/typescript/loc/lcl/CSY/Targets/TypeScriptProjectProperties.xaml.lcl 110 | node_modules/typescript/loc/lcl/CSY/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 111 | node_modules/typescript/loc/lcl/CSY/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 112 | node_modules/typescript/loc/lcl/CSY/TypeScriptTasks/TypeScript.Tasks.dll.lcl 113 | node_modules/typescript/loc/lcl/DEU/Targets/ProjectItemsSchema.xaml.lcl 114 | node_modules/typescript/loc/lcl/DEU/Targets/TypeScriptCompile.xaml.lcl 115 | node_modules/typescript/loc/lcl/DEU/Targets/TypeScriptProjectProperties.xaml.lcl 116 | node_modules/typescript/loc/lcl/DEU/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 117 | node_modules/typescript/loc/lcl/DEU/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 118 | node_modules/typescript/loc/lcl/DEU/TypeScriptTasks/TypeScript.Tasks.dll.lcl 119 | node_modules/typescript/loc/lcl/ESN/Targets/ProjectItemsSchema.xaml.lcl 120 | node_modules/typescript/loc/lcl/ESN/Targets/TypeScriptCompile.xaml.lcl 121 | node_modules/typescript/loc/lcl/ESN/Targets/TypeScriptProjectProperties.xaml.lcl 122 | node_modules/typescript/loc/lcl/ESN/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 123 | node_modules/typescript/loc/lcl/ESN/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 124 | node_modules/typescript/loc/lcl/ESN/TypeScriptTasks/TypeScript.Tasks.dll.lcl 125 | node_modules/typescript/loc/lcl/FRA/Targets/ProjectItemsSchema.xaml.lcl 126 | node_modules/typescript/loc/lcl/FRA/Targets/TypeScriptCompile.xaml.lcl 127 | node_modules/typescript/loc/lcl/FRA/Targets/TypeScriptProjectProperties.xaml.lcl 128 | node_modules/typescript/loc/lcl/FRA/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 129 | node_modules/typescript/loc/lcl/FRA/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 130 | node_modules/typescript/loc/lcl/FRA/TypeScriptTasks/TypeScript.Tasks.dll.lcl 131 | node_modules/typescript/loc/lcl/ITA/Targets/ProjectItemsSchema.xaml.lcl 132 | node_modules/typescript/loc/lcl/ITA/Targets/TypeScriptCompile.xaml.lcl 133 | node_modules/typescript/loc/lcl/ITA/Targets/TypeScriptProjectProperties.xaml.lcl 134 | node_modules/typescript/loc/lcl/ITA/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 135 | node_modules/typescript/loc/lcl/ITA/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 136 | node_modules/typescript/loc/lcl/ITA/TypeScriptTasks/TypeScript.Tasks.dll.lcl 137 | node_modules/typescript/loc/lcl/JPN/Targets/ProjectItemsSchema.xaml.lcl 138 | node_modules/typescript/loc/lcl/JPN/Targets/TypeScriptCompile.xaml.lcl 139 | node_modules/typescript/loc/lcl/JPN/Targets/TypeScriptProjectProperties.xaml.lcl 140 | node_modules/typescript/loc/lcl/JPN/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 141 | node_modules/typescript/loc/lcl/JPN/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 142 | node_modules/typescript/loc/lcl/JPN/TypeScriptTasks/TypeScript.Tasks.dll.lcl 143 | node_modules/typescript/loc/lcl/KOR/Targets/ProjectItemsSchema.xaml.lcl 144 | node_modules/typescript/loc/lcl/KOR/Targets/TypeScriptCompile.xaml.lcl 145 | node_modules/typescript/loc/lcl/KOR/Targets/TypeScriptProjectProperties.xaml.lcl 146 | node_modules/typescript/loc/lcl/KOR/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 147 | node_modules/typescript/loc/lcl/KOR/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 148 | node_modules/typescript/loc/lcl/KOR/TypeScriptTasks/TypeScript.Tasks.dll.lcl 149 | node_modules/typescript/loc/lcl/PLK/Targets/ProjectItemsSchema.xaml.lcl 150 | node_modules/typescript/loc/lcl/PLK/Targets/TypeScriptCompile.xaml.lcl 151 | node_modules/typescript/loc/lcl/PLK/Targets/TypeScriptProjectProperties.xaml.lcl 152 | node_modules/typescript/loc/lcl/PLK/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 153 | node_modules/typescript/loc/lcl/PLK/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 154 | node_modules/typescript/loc/lcl/PLK/TypeScriptTasks/TypeScript.Tasks.dll.lcl 155 | node_modules/typescript/loc/lcl/PTB/Targets/ProjectItemsSchema.xaml.lcl 156 | node_modules/typescript/loc/lcl/PTB/Targets/TypeScriptCompile.xaml.lcl 157 | node_modules/typescript/loc/lcl/PTB/Targets/TypeScriptProjectProperties.xaml.lcl 158 | node_modules/typescript/loc/lcl/PTB/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 159 | node_modules/typescript/loc/lcl/PTB/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 160 | node_modules/typescript/loc/lcl/PTB/TypeScriptTasks/TypeScript.Tasks.dll.lcl 161 | node_modules/typescript/loc/lcl/RUS/Targets/ProjectItemsSchema.xaml.lcl 162 | node_modules/typescript/loc/lcl/RUS/Targets/TypeScriptCompile.xaml.lcl 163 | node_modules/typescript/loc/lcl/RUS/Targets/TypeScriptProjectProperties.xaml.lcl 164 | node_modules/typescript/loc/lcl/RUS/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 165 | node_modules/typescript/loc/lcl/RUS/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 166 | node_modules/typescript/loc/lcl/RUS/TypeScriptTasks/TypeScript.Tasks.dll.lcl 167 | node_modules/typescript/loc/lcl/TRK/Targets/ProjectItemsSchema.xaml.lcl 168 | node_modules/typescript/loc/lcl/TRK/Targets/TypeScriptCompile.xaml.lcl 169 | node_modules/typescript/loc/lcl/TRK/Targets/TypeScriptProjectProperties.xaml.lcl 170 | node_modules/typescript/loc/lcl/TRK/TypeScriptDebugEngine/TypeScriptDebugEngine.dll.lcl 171 | node_modules/typescript/loc/lcl/TRK/TypeScriptLanguageService/Microsoft.CodeAnalysis.TypeScript.EditorFeatures.dll.lcl 172 | node_modules/typescript/loc/lcl/TRK/TypeScriptTasks/TypeScript.Tasks.dll.lcl 173 | --------------------------------------------------------------------------------