let's assume we have the following situation:
class BaseClass {
// ....some logic
}
class A extends BaseClass{
name: string;
constructor(name: string) {
this.name = name
}
someMethodFromInside() {
// ....some logic
}
}
class B extends BaseClass {
name: string;
constructor(name: string) {
this.name = name
}
someMethodFromInside() {
// ....some logic
}
}
And then i would like to choose which object of a class i'm creating, based on some string value, that i'm getting from outside:
const someStringFromOutside = 'key1'
const obj: IHandler = {
'key1': A,
'key2': B
}
try {
// here i would like to create object based on value in string choose to pick up class and create
// object
new obj[someStringFromOutside]('somevariable').someMethodFromInside()
} catch(err) {
...some error handling
}
Because i'm writing in TypeScript i also have to know how to properly type interface in handler:
I was trying to pass some kind ObjectConstructor for value in interface. But it was to generic and typescript display me error.
enum HandlerEvents {
KEY_1 = 'key1',
KEY_2 = 'key2'
}
interface IHandler {
[key in HandlerEvents]: .....what should be here ?
}
Related
In my project, I have a handful of data model classes that take a response from an API in the constructor.
Here's an example, with an arbitrary toJSON method.
class Document {
id: number
name: string
constructor(init: DocumentStructure) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
It's coming into the constructor as an object, so for correct typing I also have a type definition for the object structure that is separate from the class. Assume that it must come in as an object due to requirements further up the chain.
type DocumentStructure = {
id: number
handle: string
}
My question is: is there any way to use the class as a structural definition? Could I ever do something like the following, where the incoming init object is a JSON structure that matches the attributes of Document, but is not actual an instance of the class Document?
class Document {
id: number
name: string
constructor(init: Document) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
If this is an impossible/bad idea, what are the TS best practices for dealing with this?
I would suggest separating the classes and input structures, e.g. class Document and interface DocumentDTO:
interface DocumentDTO {
id: number
name: string
}
class Document extends DocumentDTO {
constructor(init: DocumentDTO) {
this.id = init.id
this.name = init.name
}
toJSON() {
return {
id: this.id,
name: this.name
}
}
}
If though you are in some way restricted, you can also use the following approach:
// A helper type for extractig all non-function properties from type C
type SerializableProperties<C> = {
[K in keyof C]: C[K] extends Function ? never : K;
}[keyof C];
// This type will be the same as C but it will not have any function properties
//
// If you don't need the extra generality you can also just say
//
// type Serializable<C> = Omit<C, 'toJSON'>;
//
// But that might give you some headaches when you start adding methods to those classes :)
type Serializable<C> = Pick<C, SerializableProperties<C>>;
class DocumentClass {
id: string;
name: string;
constructor(init: Serializable<DocumentClass>) {
this.id = init.id
this.name = init.name
}
toJSON(): Serializable<DocumentClass> {
return {
id: this.id,
name: this.name
}
}
}
enter link description hereTypeScript Playground link here
If you're okay with keeping all of the class parameters, you can skip the toJSON method and use the built-in
// { id: 'foo', name: 'bar' }
JSON.stringify(new Document(...))
Challenge
Below is a simplified example of how we control and pass data in an app. It is used in many places and works to translate data between UI, APIs, and a database.
API and UI use camelCase.
Database uses snake_case.
Currently, it's an awkward combination of Partial/Pick types to get some typing where...
const item = { fooBar: 'something' }
Item.cast(item).value // returns type Partial<ItemModel>
Item.create(item).value // returns type ItemModel
The goal is to return the real returned object type.
// Examples
const item = { fooBar: 'something' }
Item.cast(item).value // returns { fooBar: 'something' }
Item.cast(item).databaseFormat // returns { foo_bar: 'something' }
Item.create(item).value // returns { id: '{uuid}', fooBar: 'something' }
Item.create(item).databaseFormat // returns { id: '{uuid}', foo_bar: 'something' }
const itemFromDatabase = { id: '{uuid}', foo_bar: 'something', baz: null }
Item.cast(itemFromDatabase).value // returns { id: '{uuid}', fooBar: 'something', baz: null }
Item.cast(itemFromDatabase).databaseFormat // returns { id: '{uuid}', foo_bar: 'something', baz: null }
Any ideas on this? I would image it's something like the Object.entries() return type but I can't figure out that right T keyof combination.
// https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript
interface ObjectConstructor {
// ...
entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
// ...
}
Code
import camelcaseKeys from 'camelcase-keys'
import snakecaseKeys from 'snakecase-keys'
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
interface ItemModel {
id: string
fooBar: any
baz?: number
}
interface ItemDatabaseModel {
id: string
foo_bar: any
baz?: number
}
export class Item {
private _data: Partial<ItemModel>
public static cast(item: Partial<ItemModel | ItemDatabaseModel>): Item {
return new this(camelcaseKeys(item))
}
public static create(item: Optional<ItemModel | ItemDatabaseModel, 'id'>): Item {
// Use item.id or add item.id === null to force key
return new this(camelcaseKeys(item))
}
private constructor(input: Partial<ItemModel>) {
// Validate "input" properties have a Item class property setter, else throw
// foreach => this[key] = input[key]
}
get databaseFormat() { return snakecaseKeys(this._data) }
get value() { return this._data }
set id(value: string | null) {
// automatically generate an ID if null, otherwise validate
this._data.id = value
}
set fooBar(value: any) {
// validate
this._data.fooBar = value
}
set baz(value: number | null) {
// validate
this._data.baz = value
}
}
The best way you could achieve this would be to have some tool which goes through your typescript files looking to type/interface definitions and automatically outputs additional types with the converted keys based on what it finds.
At the moment typescript doesn't support this kind of conversion automatically via a type definition and I'd guess that they'd be quite cautious about adding something like that; concatenating string literals in type definitions is something which has yet to make it into the language, for instance, and what you're looking for here is quite a bit more complex than that, unfortunately.
This is a typescript class with an exposed but not editable attribute:
export class Category {
private _id: number;
constructor(id: number){
this._id = id;
}
public get id(){
return this._id;
}
}
I would like to map it from a JSON like this:
{ id: 1 }
There are some obvious problems here:
I know that "_id" can't be magically mapped to "id" so maybe I could implement a custom logic that renames all the attributes with a _ before the name
I would like to keep the constructor with the id param but maybe I'm not able to map an object who require arguments before instantiation
With an empty constructor, I tried Object.assign(new Category(), jsonObject), however, this does not work since Cannot set property id of #<Category> which has only a getter
What I want to avoid is to write custom mapping logic for every class like this with private attributes, I tried some other solutions and libraries but didn't work for my situation, they're all referencing to class with only public attributes
I don't even know if what I ask is achievable, so if the case it isn't, then I will "surrender" to use the class with only public attributes
The missconception here is that you need a getter/setter at all just to manage visibility. You can't prevent code from accessing and modifying id, no matter what you do. You can however tell the IDE (and the developer using it) that he can only read/get the property by using the readonly modifier, which simplifies your code to:
export class Category {
readonly id: number;
}
Noe that readonly thing only exists at compile time, and doesnt have any affects at runtime. Your IDE will prevent you from doing:
(new Category).id = 5;
But it allows you to easily do:
const category = Object.assign(new Category, { id: 5 });
Pass the object through constructor:
export class Category {
private _a: number;
private _b: string;
constructor(values: { a: number; b: string }){
this._a = values.a;
this._b = values.b;
}
public getA() { return this._a; }
public getB() { return this._b; }
}
You can still call it with or without JSON:
let cat1 = Category (values: { a: 42, b: 'hello' });
let json = '{"a":42,"b":"hello"}'
let cat2 = Category (values: JSON.parse (json));
Alternatively, keep the values in an object rather than a direct member. This makes it unnecessary to map the object to member variables:
export class Category {
private _values: {
a: number;
b: string;
}
constructor(values: { a: number; b: string }){
this._values = values;
}
public getA() { return this._values.a; }
public getB() { return this._values.b; }
}
If you want to keep the old constructor, you can do so like this:
export class Category {
private _values: {
a: number;
b: string;
}
constructor(a: number, b: string) {
this._values = { a: a, b: b };
}
public static make (values: { a: number; b: string }) {
this._values = values;
}
public getA() { return this._values.a; }
public getB() { return this._values.b; }
}
Say I have
interface Action<T> {
assignAction(key: keyof T, value: any): void;
}
Say T is of type
{
users: User[];
accounts: Account[];
}
Now, when calling assignAction, let's say I want to pass users. So this action is false because types don't match:
assignAction('users', accounts)
I don't know how to validate value, since its type depends on what you choose for key.
You should be able to add a generic to the assignAction function to help describe this relationship.
interface Action<T> {
assignAction<K extends keyof T>(key: K, value: T[K]): void;
}
Then once your Action instance is given a generic type it knows how to associate the relationship between key and value when you call assignAction
interface TypeA {
objectIdA: string
}
interface TypeB {
objectIdB: string
}
interface TypeC {
objectIdC: string
}
enum Param1 {
TypeA = "TypeA",
TypeBC = "TypeBC"
}
type INamespaceKeyMap =
& Record<Param1.TypeA, TypeA>
& Record<Param1.TypeBC, TypeB | TypeC>;
type INamespaceKeys<T extends Param1.TypeA | Param1.TypeBC> = INamespaceKeyMap[T];
function test<NS extends Param1, Key extends INamespaceKeys<NS>>(namespace: NS, key: Key) {
console.log("Called");
}
const objectA = {
objectIdA: "test"
} as TypeA;
const objectB = {
objectIdB: "test"
} as TypeB;
const objectC = {
objectIdC: "test"
} as TypeC;
test(Param1.TypeA, objectB) // not allowed
test(Param1.TypeA, objectA)
test(Param1.TypeBC, objectA) // not allowed
test(Param1.TypeBC, objectB)
test(Param1.TypeBC, objectC)
I'm using these tools together:
TypeScript
Gulp
Gulp-Inject
I'm trying to do the following:
module My {
interface IGulpInjectable extends string { // << Problem here!
[gulp_inject: string] : string;
}
export class Cache {
private items: { [key: string] : IGulpInjectable };
constructor() {
this.items = {
"item1": { gulp_inject: "file1.html" },
"item2": { gulp_inject: "file2.html" }
}
}
getItem(key: string){
return this.items[key].trim();
}
}
}
What gulp-inject does is replace { gulp_inject: "x.html" } with a string containing the file contents. This is why I want to have IGulpInjectable extend string: so that methods like trim() will be understood by TypeScript.
However, extends string is not valid. Neither is extends String. At least, not with my current constructor code, which I prefer not to change.
How can I tell TypeScript that my interface has all methods a string has?
Footnote, my current workaround:
getItem(key: string){
return (<any> this.items[key]).trim();
}
But that's not quite satisfying.
The following code works fine in typescript playground:
interface IGulpInjectable extends String
{
gulp_inject: string;
}
class Cache
{
private items: { [key: string] : IGulpInjectable };
constructor()
{
let item1 = new String(" 123 ");
item1["gulp_inject"] = "file1.html";
let item2 = new String(" 4556 ");
item2["gulp_inject"] = "file2.html";
this.items = {
"item1": <IGulpInjectable>item1,
"item2": <IGulpInjectable>item2
}
}
getItem(key: string)
{
return this.items[key];
}
}
let c = new Cache();
let i = c.getItem("item1");
console.log(i.trim()); //output '123'
console.log(i.gulp_inject); //output 'file1.html'
Link: typescript playground
Hope this helps.
Try changing to:
interface String extends String{
[gulp_inject: string] : string;
}