#Inheritance and genericity

1 messages Β· Page 1 of 1 (latest)

cold meteor
#

Hello πŸ™‚

I want to do something "somewhat easy" but I feel like I didn't get it and it will be way more complicated than that ^^"

Let's have two classes :

class Animal {}
class Cat extends Animal {}

I would like, in a generic function, check if my T derives from Animal :

function derivesFromAnimal<T>(obj: T): boolean {
 return obj instanceof Animal
}

derivesFromAnimal<Cat>(new Cat()) // false

Why is it always false ^^" ?
Is there a way to achieve that ?

Thanks πŸ™‚

gleaming pulsar
#
  • why do you need this as a function?
  • why would that be generic?
#

and what do you mean it's always false?

sacred light
#

It's not false.

What you're doing here doesn't really involve types at all. TS doesn't understand inheritance and considers Animal and Cat both to be {} at the type level.

You can just do

function derivesFromAnimal(obj: object): boolean {
    return obj instanceof Animal
}
#

If what you wanted to do what create a type guard that narrowed the type, then you have a bit of an issue because TS doesn't understand inheritance. You can use instanceof checks in TS but they aren't really type-correct. If something is an instanceof Animal it will narrow it to {}, but if it is not an instance of Animal TS will negate it, and claim that the item is not {} which may be incorrect

proud groveBOT
#
class Animal {}
class Cat extends Animal {}

function DerivesFromAnimal(obj: object): obj is Animal {
    return obj instanceof Animal
}

const cat = (Math.random() < 0.5) ? new Cat() : {}

if (DerivesFromAnimal(cat)) {
    cat
//  ^? - const cat: Cat
} else {
    cat
//  ^? - const cat: never
}
sacred light
#

So to avoid it you want to give your instances a unique structure instead

proud groveBOT
#
class Animal {
    isAnimal = true
}
class Cat extends Animal {
    isCat = true
}

function DerivesFromAnimal(obj: object): obj is Animal {
    return 'isAnimal' in obj && obj.isAnimal === true
}

const cat = (Math.random() < 0.5) ? new Cat() : {}

if (DerivesFromAnimal(cat)) {
    cat
//  ^? - const cat: Animal
} else {
    cat
//  ^? - const cat: {}
}
sacred light
#

Basically rely on the structure of objects not the class if you want the best experience, as this is how the TS type system works

narrow kernel
#

You can also avoid it by making your classes nominal one way or another. My personal favorite way is declare #animal: true - a truly private property that won't show up in intellisense and doesn't actually exist at runtime

gleaming pulsar
#

if you want to make the structure distinct for types then you have to actually make it distinct

#

using symbols or privates, for example

sacred light
narrow kernel
#

Try assigning anything else, even with the same private identifier, it'll only allow subclasses which in this case is what you want

sacred light
narrow kernel
#
declare class Foo {
    #foo: true;
}

declare class Bar {
    #foo: true;
}

const foo: Foo = new Bar(); // Errors
sacred light
#

How does that work?

gleaming pulsar
gleaming pulsar
narrow kernel
#

yeah it's modeling the same thing that happens at runtime

sacred light
#

very intestring

sacred light
#

In practice you'd only use the class, and wouldn't be creating objects of that shape in other ways.

gleaming pulsar
#

and if you have that creator, you would be using that creator instead of just fulfilling its interface.

#

your guard is pretty much for interfaces though

proud groveBOT
#
class Animal {
    declare #animal: true
//  ^^^^^^^
// 'declare' modifier cannot be used with a private identifier.
}
sacred light
#

@narrow kernel ?

narrow kernel
#

I put the declare in the wrong place

gleaming pulsar
#

instanceof narrowing is perfectly safe in the true branch

narrow kernel
#

I edited it before you posted the twoslash though

gleaming pulsar
narrow kernel
#

my bad

gleaming pulsar
sacred light
narrow kernel
#

yeah that's the best way in actual .ts files

#

at least that I know of, unless it's actually a runtime thing

#

I write .d.ts files a lot more than I write .ts files tbh

gleaming pulsar
#

if it were a runtime thing it'd probably be public anyways to actually be useful

proud groveBOT
#
class Animal {
    #Animal!: true
}
class Cat extends Animal {
    #Cat!: true
}
class Dog extends Animal {
    #Dog!: true
}

function DerivesFromAnimal(obj: object): obj is Animal {
    return obj instanceof Animal
}

const cat = (Math.random() < 0.5) ? new Cat() : {}

if (DerivesFromAnimal(cat)) {
    cat
//  ^? - const cat: Cat
} else {
    cat
//  ^? - const cat: never
}
sacred light
#

But the else block still doesn't work here

#

Did I make a mistake somewhere?

gleaming pulsar
narrow kernel
#

accept obj: unknown and it'll work

narrow kernel
sacred light
#

It's not working, but it doesn't make sense to me why

proud groveBOT
#
sandiford#0

Preview:```ts
class Animal {
_Animal!: true
}
class Cat extends Animal {
_Cat!: true
}
class Dog extends Animal {
_Dog!: true
}

function DerivesFromAnimal(
obj: unknown
): obj is Animal {
return obj instanceof Animal
}

const cat = {}

if (DerivesFromAnimal(cat)) {
...```

proud groveBOT
#
const cat: {} /* 15:7, 17:23, 21:5 */
const cat: Animal /* 18:5 */```
narrow kernel
#

This is what I see

sacred light
#

fff

#

yeah

#

OK it's fine

gleaming pulsar
#

mightve been looking at stale twoslash

sacred light
#

I must have copied it from somewhere with the type comment included

proud groveBOT
#
class Animal {
    #Animal!: true
}
class AnotherAnimal {
    #Animal!: true
}

function DerivesFromAnimal(obj: object): obj is Animal {
    return obj instanceof Animal
}

const animal = (Math.random() < 0.5) ? new Animal() : new AnotherAnimal()

if (DerivesFromAnimal(animal)) {
    animal
//  ^? - const animal: Animal
} else {
    animal
//  ^? - const animal: AnotherAnimal
}
sacred light
#

This is pretty cool πŸ‘πŸ»

#

although I use useDefineForClassFields, so I think I would want to initialize it to true

narrow kernel
#

{} has behaviours that are unexpected to most people at the type level since it actually means anything besided null or undefined

sacred light
#

sure

#

but not {} in JS

#

I guess I should have said object not {}

narrow kernel
#

const cat = (Math.random() < 0.5) ? new Cat() : {} is gonna be typed as {} still though

sacred light
#

nope

narrow kernel
#

Cat | {} just collapses down to {}

sacred light
#

well

narrow kernel
sacred light
#

is Cat object or {}?

#

yeah because the Cat interface doesn't have to be a cat instance, which is quite funny

narrow kernel
#

I'm just saying that Math.random() > 0.5 ? T : {} will always be typed as {} unless T includes null, undefined, any, or unknown

sacred light
#

const x = {} is object right?

narrow kernel
#

nope, at least not with any compiler flags I've played with

sacred light
#

This seems... wrong

proud groveBOT
#
let x = {} 
x = 1
sacred light
#

that's funny

#

might have to file that

narrow kernel
#

If you want to be more type safe I recommend explicit Record<string, never> wherever possible

#

I think they'll just reject it tbh

#

maybe they'll do it under some compiler flag

sacred light
#

const x = {} is not really used ever is it?

#

you'd declare it as a wider type to add props to it later

#

It seems basically irrelevant to ordinary code

narrow kernel
#

Maybe they'll be receptive to changing inference of {} in the value position sure

#

But they'll probably feel that Record<string, never> isn't perfect either

#

cuz it isn't

#

object still allows classes, functions, and arrays

sacred light
#

yep

#
let x = { a: 1 } 
const f = () => {}
f.a = 1
x = f
#

legal

narrow kernel
#

yeah, I do sometimes wonder why they infer f based upon usage

sacred light
#
let x: { a: number; } & object = { a: 1} 
const f = () => {}
f.a = 1
x = f
#

legal

#

Oh i'm good with f

narrow kernel
#

but not stuff like:

const x = [];
x.push(1);
#

it's still any[]

sacred light
#

Otherwise it's really hard to add props to it

narrow kernel
#

I'm glad they did it

#

I'm just confused they didn't take it more places

#

probably too breaking of a change whenever they considered it tbh

sacred light
#

Well, you can delcare x when you create it

narrow kernel
#

sure, it's just less DRY

#

not really a huge problem

#

just a mild nuisance

sacred light
#

But if you declare f as { (): undefined, a: number } then you can't satisfy that shape in a single assignment

narrow kernel
#

well you can, it's just not pretty

proud groveBOT
#
const f: { (): undefined, a: number } = () => {}
//    ^
// Property 'a' is missing in type '() => undefined' but required in type '{ (): undefined; a: number; }'.

const f2: { (): undefined, a: number } = () => {}
f2.a = 1
sacred light
#

how?

narrow kernel
#

spread new Function with some properties

sacred light
#

Can you show me?

sacred light
#

maybe I prefer the explicit type like f2 here

narrow kernel
#

Actually this is nicer

#
const f: { (): void, a: number } = Object.assign(() => {}, { a: 1 })
#

still not nice

#

but nicer than the original way I said it

sacred light
#

yeah I don't like that much

narrow kernel
#

don't disagree there

proud groveBOT
#
const x = []
//    ^? - const x: any[]

x.push(1)

for (const v of x) {
    v
//  ^? - const v: number
}
sacred light
#

So TS does infer, but it does not display well

narrow kernel
#

interesting

#

either that changed or I was fooled by bad display

sacred light
#

maybe they don't want to call it number[] because you could push a string to it

#

probably should be called inferred[] or infer[]

narrow kernel
#

hmm I've been bitten by this before in some context but tbh it's been a loooong while since I've been doing .ts files like I said already

#

so maybe it's something I'm not thinking of that was biting me back then

sacred light
#

What are you writing if not .ts?

narrow kernel
#

or maybe they fixed it

#

.d.ts

sacred light
#

But

narrow kernel
#

No function bodies

sacred light
#

are you just typing other people's code?

narrow kernel
#

Yup

sacred light
#

aha

narrow kernel
#

They decided it was a good idea to make their own runtime validation library that doesn't follow any TS principles

sacred light
#

eww

#

sounds difficult

narrow kernel
#

so I got the fun job of typing stuff like new StringField({ required: false }) which means it includes undefined

#

but also depends on StringField._defaults

#

each field can have their own defaults for required/nullable/choices/etc.

sacred light
#

Stop, you're hurting me

narrow kernel
#

tbh the hard part wasn't typing it, the hard part was reporting all of the bugs that cropped up to both them and TS itself

sacred light
#

ah

narrow kernel
#

I mean typing it was hard it took me a few days over the course of weeks in my free time to do all fields and all the places they're used

#

but digging in and finding all of the "type too deep" errors once they actually started being used involved debugging tsc to find all of the issues

sacred light
#

oucsh

narrow kernel
#

and that took a lot longer

cold meteor
#

First, thankks a lot everybody for the answers and the very interesting explanations that I'm still trying to fully grasp ^^
My issue is related to all that but was indeed not that... it seems way more complicated... let me give you some details. It's kind of linked with Adonisjs/Lucid ORM so... I'll try to take shome shortcuts.... I hope it'll be clear

I have 5 different tables, that gives me 5 models.
If I write it kind of Adonis style, all my models will be like this :

export default class Modules extends BaseModel {
@column()
declare id: UUID

#column()
declare version: boolean
//etc
async getModules() {
//extending the model within an instance
}
static async getModules() {
//extending the model at the class level
}
}

export default class Projects extends BaseModel {
// samesies
}

The thing is, my 5 tables will have 4 or 5 field in common, and some logic (around hundred lines) fully in common, to deal with a "version" systems amongst the modules and projects (long story to explain this architecture ^^")

For the fields, it's easy : I can just create an "intermediate model", called like VerisonedModel that inherit from base models, my 5 tables then inherit from VersionedModel

The problem comes when I want to "unify" the logic of the 5 tables in one.....
basic exemple :

// Query :
const module = await db.query<Module>().from("modules").first()
const otherWay = await Module.query().first()
// Lets say it's not null

// editing something
module.version = 12

// saving
module.save()

But, when I want to make it generic... something like that :

const module = await db.query<T>().from("modules").first()

Event calling a function with <Module>, it doesn't recognize any field :/
Since I know for sure that the type is "fine", I tried to take it in const module: any... everything works except the module.save() doesn't exist......

#

soooo... long story short.... I'm kind of blocked to unify this logic, since it will be the exact same across my 5 tables....

I could copy paste it 5 times and honestly there is no reason for it to go further.... but it seems dirty and I don't want to maintain the 5 logics separately ^^"

gleaming pulsar
#

seems like you'd want a constraint on your generic

#

how are you defining it

#

also fyi:

#

!:thats-no%

proud groveBOT
#
tjjfvi#0
`!tjjfvi:thats-no-generic-its-a-type-cast`:

When you use a type parameter in the function, it should generally be inferrable from the arguments.

For example, this is a misuse of generics:

function getMeA<T>(): T {
   /* magic */
}

because getMeA<string>() and getMeA<number>() compile to the same code at runtime, there's so way to implement this function safely (other than always throwing); this is just a type cast in disguise. Instead of using a generic here, you should return unknown, and cast at the call site if necessary, to be clear it's an unsafe operation:

-function getMeA<T>(): T {
+function getMeA(): unknown {
    /* magic */
 }

-getMeA<number>()
+getMeA() as number

One exception to this rule is if you're returning a possibly-empty container of T. For example, these are all perfectly safe, even though the generic can't be inferred from the parameters:

function emptyArray<T>(): Array<T> {
  return []
}

function useRef<T>(): { current?: T } {
  return {}
}
cold meteor
#

Ok I read it 10 times and I think I'm slowly getting it, lemme try something XD

#

Nah i'm still lost because :

  • db.query will return me either any or the type I pass to query<Type>
  • Since I'm calling this in my "unified" function for my 5 tables :
  • At this point I don't know the type
  • If I let it be any it doesn't know the functions (the .save())

So even if the T vs unkown makes a lot of sense and I understand why it's really being typesafe at all and I should cast the result.... since I'm two level deeps, in the level inbetween I unable to cast.... so it has to be able to understand it's the same base for the 5 tables anyway and call everything normally ^^"

Sorry I don't know if it makes perfectly sense

gleaming pulsar
cold meteor
# gleaming pulsar did you go through this

So... funny story...
I actually was, just extended T with the intermediate Model, it can theorically never breaks because I query only the tables that are allowed in a determined literals so it's safe enough it would break at the compilation first, but.....
I just discovered that the .save() doesn't work because it exists only if I query from a Model, not in the "general" way....

#

so... the whole type story is fixed, thanks a lot ^^""""

cold meteor
#

Hello πŸ™‚
Ok I'm there, it works, but I can do better.

This is exactly what I'm trying to do :

const project = Project.query() // <= return a type Project[], it's an Adonis/Lucid method

The query is as followed :

(method) LucidModel.query<typeof Project, Project(this: typeof Project, options?: ModelAdapterOptions): ModelQueryBuilderContract<typeof Project, Project>
Returns the query for fetching a model instance

I'm trying to read the implementation but it's above my head, but it's exactly the result I'm looking for : Thanks to the Project. it knows what I'm looking for

#

Here is the signature :

query<Model extends LucidModel, Result = InstanceType<Model>>(this: Model, options?: ModelAdapterOptions): ModelQueryBuilderContract<Model, Result>;