admin管理员组文章数量:1023764
Context
I am writing a schema library, where users can define a relationship between tables.
For example:
- Users can define tables
posts
,users
- Users can define a relationship:
posts
have oneowner
, which point tousers
.
Here's how that looks:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
// ---> intelisense works here
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
}
})
And here's what I have to make that work:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
}
}
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
Playground Link
Goal
I want to add a feature called reverseLinks
.
Right now we say that posts
has one owner
. But, I also want to say:
users
have manyposts
, throughposts.owner
.
This is what the new relationship would look like:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
through: 'owner',
cardinality: 'many'
}
}
}
})
Problem
But, I can't seem to get typescript to infer through
. Here's what I have so far:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
reverseLinks: any;
}
}
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through: keyof S[Namespace]['forwardLinks']
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
reverseLinks: {
[label: string]: ReverseLink<S, keyof S>
}
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'users',
cardinality: 'one',
}
},
reverseLinks: {},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
cardinality: 'many',
// ERROR Type 'string' is not assignable to type 'never'.(2322)
// Expected: 'owner'
through: 'owner'
}
}
}
})
Playground Link
How would you approach this?
Context
I am writing a schema library, where users can define a relationship between tables.
For example:
- Users can define tables
posts
,users
- Users can define a relationship:
posts
have oneowner
, which point tousers
.
Here's how that looks:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
// ---> intelisense works here
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
}
})
And here's what I have to make that work:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
}
}
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
Playground Link
Goal
I want to add a feature called reverseLinks
.
Right now we say that posts
has one owner
. But, I also want to say:
users
have manyposts
, throughposts.owner
.
This is what the new relationship would look like:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
through: 'owner',
cardinality: 'many'
}
}
}
})
Problem
But, I can't seem to get typescript to infer through
. Here's what I have so far:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
reverseLinks: any;
}
}
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through: keyof S[Namespace]['forwardLinks']
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
reverseLinks: {
[label: string]: ReverseLink<S, keyof S>
}
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'users',
cardinality: 'one',
}
},
reverseLinks: {},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
cardinality: 'many',
// ERROR Type 'string' is not assignable to type 'never'.(2322)
// Expected: 'owner'
through: 'owner'
}
}
}
})
Playground Link
How would you approach this?
Share Improve this question edited Nov 19, 2024 at 18:32 Stepan Parunashvili asked Nov 19, 2024 at 0:10 Stepan ParunashviliStepan Parunashvili 2,8456 gold badges35 silver badges57 bronze badges 4 |2 Answers
Reset to default 1It looks like the problem here is that ReverseLink<S, N>
is not distributive over unions in N
. If you write ReverseLink<S, N1 | N2>
, you'd like it to be equivalent to ReverseLink<S, N1> | ReverseLink<S, N2>
so that the through
property always corresponds to the specific to
property. As it is now, ReverseLink<S, N1 | N2>
gives you a through
property like keyof S[N1 | N2]['forwardLinks']
, which will end up being keyof (S[N1]['forwardLinks'] | S[N2]['forwardLinks'])
. But the keyof
operator is contravariant in its operand (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript), so keyof (X | Y)
is equivalent to keyof X & keyof Y
. (See Is it possible to get the keys from a union of objects?) and the union of types corresponds to an intersection of keys, and if no keys are shared then it becomes the impossible never
type, as you saw.
There are a few ways to make types distributive over unions in TypeScript. When the type to distribute over is keylike, I tend to prefer a distributive object type as coined in microsoft/TypeScript#47109, which is just a mapped type over each member of the union into which you index with the full union:
type ReverseLink<S extends ISchema, N extends keyof S> = { [K in N]:
{
to: K;
cardinality: Cardinality;
through: keyof S[K]['forwardLinks']
}
}[N]
You can verify that if N
is a single key, then it's the same as your old version, whereas if N
is a union (like keyof S
) then this becomes a union of ReversLink<K>
for each K in N
.
Now let's test out your example:
const s = schema({
posts: {
attrs: { title: 'string' },
forwardLinks: { owner: { to: 'users', cardinality: 'one', } },
reverseLinks: {},
},
users: {
attrs: { email: 'string' },
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts', cardinality: 'many',
through: 'owner' // okay
}
}
}
})
Now it works; the type of through
is seen as having to be "owner"
. You can also verify that this works to correlate to
with through
, so you can't mix them up:
const t = schema({
a: {
attrs: {}, forwardLinks: {
x: { to: 'c', cardinality: 'one' }
}, reverseLinks: {}
},
b: {
attrs: {}, forwardLinks: {
y: { to: 'c', cardinality: 'one' }
}, reverseLinks: {}
},
c: {
attrs: {}, forwardLinks: {}, reverseLinks: {
ax: { cardinality: 'many', to: 'a', through: 'x' }, // okay
by: { cardinality: 'many', to: 'b', through: 'y' }, // okay
ay: { cardinality: 'many', to: 'a', through: 'y' } // error!
}
}
})
Playground link to code
Try to define type of through?
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through?: string
}
Context
I am writing a schema library, where users can define a relationship between tables.
For example:
- Users can define tables
posts
,users
- Users can define a relationship:
posts
have oneowner
, which point tousers
.
Here's how that looks:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
// ---> intelisense works here
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
}
})
And here's what I have to make that work:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
}
}
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
Playground Link
Goal
I want to add a feature called reverseLinks
.
Right now we say that posts
has one owner
. But, I also want to say:
users
have manyposts
, throughposts.owner
.
This is what the new relationship would look like:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
through: 'owner',
cardinality: 'many'
}
}
}
})
Problem
But, I can't seem to get typescript to infer through
. Here's what I have so far:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
reverseLinks: any;
}
}
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through: keyof S[Namespace]['forwardLinks']
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
reverseLinks: {
[label: string]: ReverseLink<S, keyof S>
}
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'users',
cardinality: 'one',
}
},
reverseLinks: {},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
cardinality: 'many',
// ERROR Type 'string' is not assignable to type 'never'.(2322)
// Expected: 'owner'
through: 'owner'
}
}
}
})
Playground Link
How would you approach this?
Context
I am writing a schema library, where users can define a relationship between tables.
For example:
- Users can define tables
posts
,users
- Users can define a relationship:
posts
have oneowner
, which point tousers
.
Here's how that looks:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
// ---> intelisense works here
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
}
})
And here's what I have to make that work:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
}
}
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
Playground Link
Goal
I want to add a feature called reverseLinks
.
Right now we say that posts
has one owner
. But, I also want to say:
users
have manyposts
, throughposts.owner
.
This is what the new relationship would look like:
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'posts',
cardinality: 'one',
}
},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
through: 'owner',
cardinality: 'many'
}
}
}
})
Problem
But, I can't seem to get typescript to infer through
. Here's what I have so far:
type Cardinality = 'many' | 'one';
type AttrType = 'string' | 'number';
type ForwardLink<Namespace> = {
to: Namespace,
cardinality: Cardinality
}
interface ISchema {
[table: string]: {
attrs: any;
forwardLinks: any;
reverseLinks: any;
}
}
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through: keyof S[Namespace]['forwardLinks']
}
type Entity<S extends ISchema> = {
attrs: {
[label: string]: AttrType
},
forwardLinks: {
[label: string]: ForwardLink<keyof S>
},
reverseLinks: {
[label: string]: ReverseLink<S, keyof S>
}
}
function schema<S extends {
[table: string]: Entity<S>
}>(s: S) {
return s;
}
const s = schema({
posts: {
attrs: {
title: 'string'
},
forwardLinks: {
owner: {
to: 'users',
cardinality: 'one',
}
},
reverseLinks: {},
},
users: {
attrs: {
email: 'string'
},
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts',
cardinality: 'many',
// ERROR Type 'string' is not assignable to type 'never'.(2322)
// Expected: 'owner'
through: 'owner'
}
}
}
})
Playground Link
How would you approach this?
Share Improve this question edited Nov 19, 2024 at 18:32 Stepan Parunashvili asked Nov 19, 2024 at 0:10 Stepan ParunashviliStepan Parunashvili 2,8456 gold badges35 silver badges57 bronze badges 4- The second playground link is corrupted. – jcalz Commented Nov 19, 2024 at 3:38
-
(see prev comment) I'd say your problem is that
ReverseLink<S, N>
needs to distribute over unions inN
. Right now you're asking for properties common to allS[N]['forwardLinks']
but those are unlikely to be anything butnever
. If you makeReverseLink
a distributive object type as shown in this playground link then you get more reasonable behavior. Note that such recursive inference is likely to eventually fail you, so you might immediately hit another problem. But, does this address the current question? If so, I'll write an answer; if not, what's missing? – jcalz Commented Nov 19, 2024 at 3:47 - This works great, thank you @jcalz! Would appreciate if you wrote the answer. If you have the bandwidth, I would love more explanation on 'such recursive inference is likely to eventually fail you, so you might immediately hit another problem. ' – Stepan Parunashvili Commented Nov 19, 2024 at 18:33
- I don't have more general explanation without an example; I'm saying... TS's inference algorithm has limitations, and the more complicated your requirements, the more chance of hitting those limits. You might find that at some point you need to give up on inference and start annotating types. But that's out of the scope of the question. I'm just trying to forestall the: "yes, this works, it allowed me to make progress until I hit something else five minutes later, which is now a showstopper for me". Other than intuition I don't have anything more concrete to say about it. – jcalz Commented Nov 19, 2024 at 19:10
2 Answers
Reset to default 1It looks like the problem here is that ReverseLink<S, N>
is not distributive over unions in N
. If you write ReverseLink<S, N1 | N2>
, you'd like it to be equivalent to ReverseLink<S, N1> | ReverseLink<S, N2>
so that the through
property always corresponds to the specific to
property. As it is now, ReverseLink<S, N1 | N2>
gives you a through
property like keyof S[N1 | N2]['forwardLinks']
, which will end up being keyof (S[N1]['forwardLinks'] | S[N2]['forwardLinks'])
. But the keyof
operator is contravariant in its operand (see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript), so keyof (X | Y)
is equivalent to keyof X & keyof Y
. (See Is it possible to get the keys from a union of objects?) and the union of types corresponds to an intersection of keys, and if no keys are shared then it becomes the impossible never
type, as you saw.
There are a few ways to make types distributive over unions in TypeScript. When the type to distribute over is keylike, I tend to prefer a distributive object type as coined in microsoft/TypeScript#47109, which is just a mapped type over each member of the union into which you index with the full union:
type ReverseLink<S extends ISchema, N extends keyof S> = { [K in N]:
{
to: K;
cardinality: Cardinality;
through: keyof S[K]['forwardLinks']
}
}[N]
You can verify that if N
is a single key, then it's the same as your old version, whereas if N
is a union (like keyof S
) then this becomes a union of ReversLink<K>
for each K in N
.
Now let's test out your example:
const s = schema({
posts: {
attrs: { title: 'string' },
forwardLinks: { owner: { to: 'users', cardinality: 'one', } },
reverseLinks: {},
},
users: {
attrs: { email: 'string' },
forwardLinks: {},
reverseLinks: {
ownedPosts: {
to: 'posts', cardinality: 'many',
through: 'owner' // okay
}
}
}
})
Now it works; the type of through
is seen as having to be "owner"
. You can also verify that this works to correlate to
with through
, so you can't mix them up:
const t = schema({
a: {
attrs: {}, forwardLinks: {
x: { to: 'c', cardinality: 'one' }
}, reverseLinks: {}
},
b: {
attrs: {}, forwardLinks: {
y: { to: 'c', cardinality: 'one' }
}, reverseLinks: {}
},
c: {
attrs: {}, forwardLinks: {}, reverseLinks: {
ax: { cardinality: 'many', to: 'a', through: 'x' }, // okay
by: { cardinality: 'many', to: 'b', through: 'y' }, // okay
ay: { cardinality: 'many', to: 'a', through: 'y' } // error!
}
}
})
Playground link to code
Try to define type of through?
type ReverseLink<S extends ISchema, Namespace extends keyof S> = {
to: Namespace;
cardinality: Cardinality;
through?: string
}
本文标签: typescriptHow to correctly infer dependent properties in recursive TS typesStack Overflow
版权声明:本文标题:typescript - How to correctly infer dependent properties in recursive TS types - Stack Overflow 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/questions/1745588401a2157738.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
ReverseLink<S, N>
needs to distribute over unions inN
. Right now you're asking for properties common to allS[N]['forwardLinks']
but those are unlikely to be anything butnever
. If you makeReverseLink
a distributive object type as shown in this playground link then you get more reasonable behavior. Note that such recursive inference is likely to eventually fail you, so you might immediately hit another problem. But, does this address the current question? If so, I'll write an answer; if not, what's missing? – jcalz Commented Nov 19, 2024 at 3:47