Closed Qsppl closed 7 months ago
In this case, an error also occurs. In reality, a handle can return undefined, but the types say that a handle always returns a model.
import { Model, define, store } from "https://esm.sh/hybrids@8.2.11"
export interface IModel {
prop1: number
}
export const ModelStore: Model<IModel> = {
id: true,
prop1: 1
}
export interface IComponent extends HTMLElement {
model: undefined | IModel
}
export default define<IComponent>({
tag: "a-component",
model: store(ModelStore),
})
message:
Type 'Descriptor<IComponent, IModel>' is not assignable to type 'Property<IComponent, IModel | undefined>'.
Type 'Descriptor<IComponent, IModel>' is not assignable to type 'Descriptor<IComponent, IModel | undefined>'.
Types of property 'set' are incompatible.
Type '((host: IComponent & HTMLElement, value: any, lastValue: IModel) => any) | undefined' is not assignable to type '((host: IComponent & HTMLElement, value: any, lastValue: IModel | undefined) => any) | undefined'.
Type '(host: IComponent & HTMLElement, value: any, lastValue: IModel) => any' is not assignable to type '(host: IComponent & HTMLElement, value: any, lastValue: IModel | undefined) => any'.
Types of parameters 'lastValue' and 'lastValue' are incompatible.
Type 'IModel | undefined' is not assignable to type 'IModel'.
Type 'undefined' is not assignable to type 'IModel'. ts(2322)
Sadly, it's a breaking change in TypeScript v5.0.0
For now, please use the latest v4
version of TS. I suppose that changing the definition for the v5
will break it for earlier versions, so I am confused about what to do with that. Do you know if there is a way to specify the TS version, which is supported by the project?
I looked in other node modules:
They all have the following content in package.json:
{
...
"main": "",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
"directory": "types/bootstrap"
},
"typesPublisherContentHash": "c92ed4379251cc173e28be43e85f18041aa3e6ec2cd0d7b66e800b0f8a0b36a8",
"typeScriptVersion": "4.5"
}
I don’t know how it works, but most likely something similar is described in DefinitelyTyped documentation.
on typescript version: 4.9.5
Add id: string
to your IModel
definition. Currently, the store()
type is used for the singleton model definition.
thanks, it works
But if you use store(ModelStore, { id: 'modelId' })
, then the problem appears again.
import { Model, define, store } from "https://esm.sh/hybrids@8.2.11"
export interface IModel {
id: number
prop1: number
}
export const ModelStore: Model<IModel> = {
id: true,
prop1: 1
}
export interface IComponent extends HTMLElement {
modelId: undefined | number
model: undefined | IModel
}
export default define<IComponent>({
tag: "a-component",
modelId: undefined,
model: store(ModelStore, { id: 'modelId' }),
})
Ok, I added this case: https://github.com/hybridsjs/hybrids/commit/c15da309ae227a596f9f85e3ba759ee814e25b04
thanks, it works
This change causes an error when using store(ModelStore, { draft: true })
.
import { Model, define, store } from "https://esm.sh/hybrids@8.2.11"
export interface IModel {
id: number
prop1: number
}
export const ModelStore: Model<IModel> = {
id: true,
prop1: 1
}
export interface IComponent extends HTMLElement {
modelId: undefined | number
model: undefined | IModel
draft: IModel
}
export default define<IComponent>({
tag: "a-component",
modelId: undefined,
model: store(ModelStore, { id: 'modelId' }),
draft: store(ModelStore, { draft: true }),
})
Updated 20.03.2024 21:25
I seem to have solved this problem using overload:
// DELETED
// function store<E, M>(
// Model: Model<M>,
// ): Descriptor<E, M extends { id: any } ? M | undefined : M>
// function store<E, M>(
// Model: Model<M>,
// options: {
// id?: keyof E | ((host: E) => ModelIdentifier)
// draft?: boolean
// },
// ): Descriptor<E, M extends { id: any } ? M | undefined : M>
/** Enumerables and Listings */
export function store<E, M extends { id: any }>(
Model: [Model<M>],
options?: { id?: keyof E | ((host: E) => ModelIdentifier) },
): Descriptor<E, M[]>
/** Draft by Model */
export function store<E, M>(
Model: Model<M>,
options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) },
): Descriptor<E, M>
/** Model */
export function store<E, M>(
Model: Model<M>,
options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) },
): Descriptor<E, M extends { id: any } ? M | undefined : M>
Here is an example to run the test:
import { Model, define, store } from "https://esm.sh/hybrids@8.2.11"
export interface ISingleton {
prop1: number
}
export const SingletonStore: Model<ISingleton> = {
prop1: 0
}
export interface IModel {
id: number
prop1: number
}
export const ModelStore: Model<IModel> = {
id: true,
prop1: 1
}
export interface IComponent extends HTMLElement {
modelId: undefined | number
singleton: ISingleton
singletonDraft: ISingleton
model: undefined | IModel
modelDraft: IModel
singletonById: ISingleton
singletonDraftById: ISingleton
modelById: undefined | IModel
modelDraftById: IModel
singletonList: ISingleton[]
singletonDraftList: ISingleton[]
modelList: IModel[]
modelDraftList: IModel[]
singletonListById: ISingleton[]
singletonDraftListById: ISingleton[]
modelListById: IModel[]
modelDraftListById: IModel[]
}
export default define<IComponent>({
tag: "a-component",
modelId: 0,
singleton: store(SingletonStore),
singletonDraft: store(SingletonStore, { draft: true }),
model: store(ModelStore),
modelDraft: store(ModelStore, { draft: true }),
singletonById: store(SingletonStore, { id: 'modelId' }),
singletonDraftById: store(SingletonStore, { id: 'modelId', draft: true }),
modelById: store(ModelStore, { id: 'modelId' }),
modelDraftById: store(ModelStore, { draft: true, id: 'modelId' }),
/// @ts-expect-error
singletonList: store([SingletonStore]),
/// @ts-expect-error
singletonDraftList: store([SingletonStore], { draft: true }),
modelList: store([ModelStore]),
modelDraftList: store([ModelStore], { draft: true }),
/// @ts-expect-error
singletonListById: store([SingletonStore], { id: 'modelId' }),
/// @ts-expect-error
singletonDraftListById: store([SingletonStore], { id: 'modelId', draft: true }),
modelListById: store([ModelStore], { id: 'modelId' }),
modelDraftListById: store([ModelStore], { id: 'modelId', draft: true }),
})
But I couldn't define the Model
argument as an object, so the list of singletons goes into the corresponding overloads, but I expect that such an argument should return an error:
Please note that if you leave JsDoc comments, they will be displayed to users in the code.
Good catch!
I should add some tests for those types, it must be possible (I'll add this to my todo list).
I updated the lastest commit with the overload version. You are always welcome to make a PR with a change, so you will have a chance to be a contributor ;)
I fixed the problem with the store([SingletonStore], ...)
overload
/** Model */
function store<E, M extends { id: any } & object>(model: Model<M>, options?: { draft?: false, id?: keyof E | ((host: E) => number) }): Descriptor<E, M | undefined>
/** Model Draft */
function store<E, M extends { id: any } & object>(model: Model<M>, options: { draft: true, id?: keyof E | ((host: E) => number) }): Descriptor<E, M>
/** Model Listing */
function store<E, M extends { id: any } & object>(model: [Model<M>], options?: { draft?: false, id?: keyof E | ((host: E) => number) }): Descriptor<E, M[]>
/** Model Listing Draft */
function store<E, M extends { id: any } & object>(model: [Model<M>], options: { draft: true, id?: keyof E | ((host: E) => number) }): Descriptor<E, M[]>
/** Singleton */
function store<E, M extends { id?: never } & object>(model: Model<M> extends Array<any> ? never : Model<M>, options?: { draft?: false, id?: keyof E | ((host: E) => number) }): Descriptor<E, M>
/** Singleton Draft */
function store<E, M extends { id?: never } & object>(model: Model<M> extends Array<any> ? never : Model<M>, options: { draft: true, id?: keyof E | ((host: E) => number) }): Descriptor<E, M>
// Singleton Listing cannot exist!
Here is a new typing test for all invariants of this function:
import { Model, define, store } from "https://esm.sh/hybrids@8.2.11"
export interface ISingleton {
prop: number
length: number
}
export const SingletonStore: Model<ISingleton> = {
prop: 0,
length: 0,
}
export interface IModel {
id: number
prop: number
length: number
}
export const ModelStore: Model<IModel> = {
id: true,
prop: 0,
length: 0,
}
export interface IComponent extends HTMLElement {
modelId: undefined | number
singleton: ISingleton
singletonDraft: ISingleton
model: undefined | IModel
modelDraft: IModel
singletonById: ISingleton
singletonDraftById: ISingleton
modelById: undefined | IModel
modelDraftById: IModel
singletonList: ISingleton[]
singletonDraftList: ISingleton[]
modelList: IModel[]
modelDraftList: IModel[]
singletonListById: ISingleton[]
singletonDraftListById: ISingleton[]
modelListById: IModel[]
modelDraftListById: IModel[]
}
export default define<IComponent>({
tag: "a-component",
modelId: 0,
singleton: store(SingletonStore),
singletonDraft: store(SingletonStore, { draft: true }),
model: store(ModelStore),
modelDraft: store(ModelStore, { draft: true }),
singletonById: store(SingletonStore, { id: 'modelId' }),
singletonDraftById: store(SingletonStore, { id: 'modelId', draft: true }),
modelById: store(ModelStore, { id: 'modelId' }),
modelDraftById: store(ModelStore, { draft: true, id: 'modelId' }),
/// @ts-expect-error singleton cannot be list
singletonList: store([SingletonStore]),
/// @ts-expect-error singleton cannot be list
singletonDraftList: store([SingletonStore], { draft: true }),
modelList: store([ModelStore]),
modelDraftList: store([ModelStore], { draft: true }),
/// @ts-expect-error singleton cannot be list
singletonListById: store([SingletonStore], { id: 'modelId' }),
/// @ts-expect-error singleton cannot be list
singletonDraftListById: store([SingletonStore], { id: 'modelId', draft: true }),
modelListById: store([ModelStore], { id: 'modelId' }),
modelDraftListById: store([ModelStore], { id: 'modelId', draft: true }),
})
Thank you, in the future I will send PRs for bugs that I can fix
Your solution is almost perfect - listing does not work with draft, so there are 5 cases, not 6:
// Enumerable
function store<E, M extends { id: string } & object>(
model: Model<M>,
options?: { draft?: false; id?: keyof E | ((host: E) => number) },
): Descriptor<E, M | undefined>;
// Enumerable Draft
function store<E, M extends { id: string } & object>(
model: Model<M>,
options: { draft: true; id?: keyof E | ((host: E) => number) },
): Descriptor<E, M>;
// Enumerable Listing
function store<E, M extends { id: string } & object>(
model: [Model<M>],
options?: { draft?: false; id?: keyof E | ((host: E) => number) },
): Descriptor<E, M[]>;
// Singleton
function store<E, M extends { id?: never } & object>(
model: Model<M> extends Array<any> ? never : Model<M>,
options?: { draft?: false; id?: keyof E | ((host: E) => number) },
): Descriptor<E, M>;
// Singleton Draft
function store<E, M extends { id?: never } & object>(
model: Model<M> extends Array<any> ? never : Model<M>,
options: { draft: true; id?: keyof E | ((host: E) => number) },
): Descriptor<E, M>;
I was trying to protect from passing a model definition without id: true
for a listening case, but this is a value, not a type - It would require adding an additional type like EnumerableModel
, which then forces to set id: true
. I gave up .. :P
However, with this change and another fixed issue, it should be possible to use TS v5.0+
again. Let me know if I am wrong.
I was trying to protect from passing a model definition without id: true for a listening case, but this is a value, not a type - It would require adding an additional type like EnumerableModel, which then forces to set id: true. I gave up .. :P
Here's proof that it works:
import { Model, ModelIdentifier } from "https://esm.sh/hybrids@8.2.11"
interface IEnumerableModel { id: any, prop: number }
interface ISingletonModel { prop: number }
// ################################### STORE BY `{ ... }` ###################################
const EnumerableInstance: IEnumerableModel = { id: true, prop: 0 }
const SingletonInstance: ISingletonModel = { prop: 0 }
/** Model */
function store<E, M extends { id: any } & object>(model: M, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M | undefined
/** Model Draft */
function store<E, M extends { id: any } & object>(model: M, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Model Listing */
function store<E, M extends { id: any } & object>(model: [M], options?: { id?: keyof E | ((host: E) => ModelIdentifier), loose?: boolean }): [M]
// Model Listing Draft cannot exist!
/** Singleton */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : M, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Singleton Draft */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : M, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
// Singleton listing cannot exist!
function store(model: any, options?: any): any { }
const modelId: undefined | number = 0
const singleton: ISingletonModel = store(SingletonInstance)
const singletonDraft: ISingletonModel = store(SingletonInstance, { draft: true })
const model: undefined | IEnumerableModel = store(EnumerableInstance)
const modelDraft: IEnumerableModel = store(EnumerableInstance, { draft: true })
const singletonById: ISingletonModel = store(SingletonInstance, { id: 'modelId' })
const singletonDraftById: ISingletonModel = store(SingletonInstance, { id: 'modelId', draft: true })
const modelById: undefined | IEnumerableModel = store(EnumerableInstance, { id: 'modelId' })
const modelDraftById: IEnumerableModel = store(EnumerableInstance, { draft: true, id: 'modelId' })
const singletonList: ISingletonModel[] = store([SingletonInstance])
const singletonDraftList: ISingletonModel[] = store([SingletonInstance], { draft: true })
const modelList: IEnumerableModel[] = store([EnumerableInstance])
const modelDraftList: IEnumerableModel[] = store([EnumerableInstance], { draft: true })
const singletonListById: ISingletonModel[] = store([SingletonInstance], { id: 'modelId' })
const singletonDraftListById: ISingletonModel[] = store([SingletonInstance], { id: 'modelId', draft: true })
const modelListById: IEnumerableModel[] = store([EnumerableInstance], { id: 'modelId' })
const modelDraftListById: IEnumerableModel[] = store([EnumerableInstance], { id: 'modelId', draft: true })
You can't do this because of another bug:
// This does not cause an error!
// This code should return the error "Model cannot be an array"
// Due to the fact that Model can be an array, typing breaks down in other places!
const Model: Model<any> = []
Here is proof that the problem is with the Model type:
import { Model, ModelIdentifier } from "https://esm.sh/hybrids@8.2.11"
interface IEnumerableModel { id: any, prop: number }
interface ISingletonModel { prop: number }
// ################################### STORE BY `Model<{ ... }>` ###################################
const EnumerableStore: Model<IEnumerableModel> = { id: true, prop: 0 }
const SingletonStore: Model<ISingletonModel> = { prop: 0 }
// This does not cause an error!
// This code should return the error "Model cannot be an array"
// Due to the fact that Model can be an array, typing breaks down in other places!
const Model: Model<any> = []
/** Model */
function store<E, M extends { id: any } & object = never>(model: Model<M>, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M | undefined
/** Model Draft */
function store<E, M extends { id: any } & object = never>(model: Model<M>, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Model Listing */
function store<E, M extends { id: any } & object>(model: [Model<M>], options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier), loose?: boolean }): [M]
// Model Listing Draft cannot exist!
/** Singleton */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : Model<M>, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Singleton Draft */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : Model<M>, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
// Singleton listing cannot exist!
function store(model: any, options?: any): any { }
const modelId: undefined | number = 0
const singleton: ISingletonModel = store(SingletonStore)
const singletonDraft: ISingletonModel = store(SingletonStore, { draft: true })
const model: undefined | IEnumerableModel = store(EnumerableStore)
const modelDraft: IEnumerableModel = store(EnumerableStore, { draft: true })
const singletonById: ISingletonModel = store(SingletonStore, { id: 'modelId' })
const singletonDraftById: ISingletonModel = store(SingletonStore, { id: 'modelId', draft: true })
const modelById: undefined | IEnumerableModel = store(EnumerableStore, { id: 'modelId' })
const modelDraftById: IEnumerableModel = store(EnumerableStore, { draft: true, id: 'modelId' })
const singletonList: ISingletonModel[] = store([SingletonStore])
const singletonDraftList: ISingletonModel[] = store([SingletonStore], { draft: true })
const modelList: IEnumerableModel[] = store([EnumerableStore])
const modelDraftList: IEnumerableModel[] = store([EnumerableStore], { draft: true })
const singletonListById: ISingletonModel[] = store([SingletonStore], { id: 'modelId' })
const singletonDraftListById: ISingletonModel[] = store([SingletonStore], { id: 'modelId', draft: true })
const modelListById: IEnumerableModel[] = store([EnumerableStore], { id: 'modelId' })
const modelDraftListById: IEnumerableModel[] = store([EnumerableStore], { id: 'modelId', draft: true })
If in the same code you use any other type instead of Model, then everything works.
import { Model, ModelIdentifier } from "https://esm.sh/hybrids@8.2.11"
interface IEnumerableModel { id: any, prop: number }
interface ISingletonModel { prop: number }
// ################################### STORE BY `Type<{ ... }>` ###################################
type Type<M> = { a: M }
const EnumerableInstance: Type<IEnumerableModel> = { a: { id: true, prop: 0 } }
const SingletonInstance: Type<ISingletonModel> = { a: { prop: 0 } }
/** Model */
function store<E, M extends { id: any } & object>(model: Type<M>, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M | undefined
/** Model Draft */
function store<E, M extends { id: any } & object>(model: Type<M>, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Model Listing */
function store<E, M extends { id: any } & object>(model: [Type<M>], options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier), loose?: boolean }): [M]
// Model Listing Draft cannot exist!
/** Singleton */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : Type<M>, options?: { draft?: false, id?: keyof E | ((host: E) => ModelIdentifier) }): M
/** Singleton Draft */
function store<E, M extends { id?: never } & object>(model: M extends Array<any> ? never : Type<M>, options: { draft: true, id?: keyof E | ((host: E) => ModelIdentifier) }): M
// Singleton listing cannot exist!
function store(model: any, options?: any): any { }
const modelId: undefined | number = 0
const singleton: ISingletonModel = store(SingletonInstance)
const singletonDraft: ISingletonModel = store(SingletonInstance, { draft: true })
const model: undefined | IEnumerableModel = store(EnumerableInstance)
const modelDraft: IEnumerableModel = store(EnumerableInstance, { draft: true })
const singletonById: ISingletonModel = store(SingletonInstance, { id: 'modelId' })
const singletonDraftById: ISingletonModel = store(SingletonInstance, { id: 'modelId', draft: true })
const modelById: undefined | IEnumerableModel = store(EnumerableInstance, { id: 'modelId' })
const modelDraftById: IEnumerableModel = store(EnumerableInstance, { draft: true, id: 'modelId' })
const singletonList: ISingletonModel[] = store([SingletonInstance])
const singletonDraftList: ISingletonModel[] = store([SingletonInstance], { draft: true })
const modelList: IEnumerableModel[] = store([EnumerableInstance])
const modelDraftList: IEnumerableModel[] = store([EnumerableInstance], { draft: true })
const singletonListById: ISingletonModel[] = store([SingletonInstance], { id: 'modelId' })
const singletonDraftListById: ISingletonModel[] = store([SingletonInstance], { id: 'modelId', draft: true })
const modelListById: IEnumerableModel[] = store([EnumerableInstance], { id: 'modelId' })
const modelDraftListById: IEnumerableModel[] = store([EnumerableInstance], { id: 'modelId', draft: true })
In short, for me, the most important is to ensure that the correct case works in TypeScript - this is essentially required to make it work with TS.
Preventing from passing wrong arguments is great, but I would say "nice to have". The library protects from it and throws errors in runtime.
BTW, Your examples are so detailed, that I see them hard to reason about and focus on a problem that you want to address.
There were some bugs in the latest changes, so other stuff missing, please try out v8.2.14
.
Problem solved.
I also sent to PR a version of the store()
function, protected from passing arguments like store([SingletonStore])
.
commit: https://github.com/hybridsjs/hybrids/commit/64777f5e1afbdd58d28767a0b6ef7dc227c121dc
message:
tsconfig.json
typescript version: "5.1.6"